Doctrine2: Best way to handle many-to-many with extra columns in reference table

后端 未结 14 1983
灰色年华
灰色年华 2020-11-22 10:44

I\'m wondering what\'s the best, the cleanest and the most simply way to work with many-to-many relations in Doctrine2.

Let\'s assume that we\'ve got an album like

相关标签:
14条回答
  • 2020-11-22 11:21

    I was getting from a conflict with join table defined in an association class ( with additional custom fields ) annotation and a join table defined in a many-to-many annotation.

    The mapping definitions in two entities with a direct many-to-many relationship appeared to result in the automatic creation of the join table using the 'joinTable' annotation. However the join table was already defined by an annotation in its underlying entity class and I wanted it to use this association entity class's own field definitions so as to extend the join table with additional custom fields.

    The explanation and solution is that identified by FMaz008 above. In my situation, it was thanks to this post in the forum 'Doctrine Annotation Question'. This post draws attention to the Doctrine documentation regarding ManyToMany Uni-directional relationships. Look at the note regarding the approach of using an 'association entity class' thus replacing the many-to-many annotation mapping directly between two main entity classes with a one-to-many annotation in the main entity classes and two 'many-to-one' annotations in the associative entity class. There is an example provided in this forum post Association models with extra fields:

    public class Person {
    
      /** @OneToMany(targetEntity="AssignedItems", mappedBy="person") */
      private $assignedItems;
    
    }
    
    public class Items {
    
        /** @OneToMany(targetEntity="AssignedItems", mappedBy="item") */
        private $assignedPeople;
    }
    
    public class AssignedItems {
    
        /** @ManyToOne(targetEntity="Person")
        * @JoinColumn(name="person_id", referencedColumnName="id")
        */
    private $person;
    
        /** @ManyToOne(targetEntity="Item")
        * @JoinColumn(name="item_id", referencedColumnName="id")
        */
    private $item;
    
    }
    
    0 讨论(0)
  • 2020-11-22 11:21

    Unidirectional. Just add the inversedBy:(Foreign Column Name) to make it Bidirectional.

    # config/yaml/ProductStore.dcm.yml
    ProductStore:
      type: entity
      id:
        product:
          associationKey: true
        store:
          associationKey: true
      fields:
        status:
          type: integer(1)
        createdAt:
          type: datetime
        updatedAt:
          type: datetime
      manyToOne:
        product:
          targetEntity: Product
          joinColumn:
            name: product_id
            referencedColumnName: id
        store:
          targetEntity: Store
          joinColumn:
            name: store_id
            referencedColumnName: id
    

    I hope it helps. See you.

    0 讨论(0)
  • 2020-11-22 11:24

    The solution is in the documentation of Doctrine. In the FAQ you can see this :

    http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table

    And the tutorial is here :

    http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html

    So you do not anymore do a manyToMany but you have to create an extra Entity and put manyToOne to your two entities.

    ADD for @f00bar comment :

    it's simple, you have just to to do something like this :

    Article  1--N  ArticleTag  N--1  Tag
    

    So you create an entity ArticleTag

    ArticleTag:
      type: entity
      id:
        id:
          type: integer
          generator:
            strategy: AUTO
      manyToOne:
        article:
          targetEntity: Article
          inversedBy: articleTags
      fields: 
        # your extra fields here
      manyToOne:
        tag:
          targetEntity: Tag
          inversedBy: articleTags
    

    I hope it helps

    0 讨论(0)
  • 2020-11-22 11:27

    What you are referring to is metadata, data about data. I had this same issue for the project I am currently working on and had to spend some time trying to figure it out. It's too much information to post here, but below are two links you may find useful. They do reference the Symfony framework, but are based on the Doctrine ORM.

    http://melikedev.com/2010/04/06/symfony-saving-metadata-during-form-save-sort-ids/

    http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/

    Good luck, and nice Metallica references!

    0 讨论(0)
  • 2020-11-22 11:29

    From $album->getTrackList() you will alwas get "AlbumTrackReference" entities back, so what about adding methods from the Track and proxy?

    class AlbumTrackReference
    {
        public function getTitle()
        {
            return $this->getTrack()->getTitle();
        }
    
        public function getDuration()
        {
            return $this->getTrack()->getDuration();
        }
    }
    

    This way your loop simplifies considerably, aswell as all other code related to looping the tracks of an album, since all methods are just proxied inside AlbumTrakcReference:

    foreach ($album->getTracklist() as $track) {
        echo sprintf("\t#%d - %-20s (%s) %s\n", 
            $track->getPosition(),
            $track->getTitle(),
            $track->getDuration()->format('H:i:s'),
            $track->isPromoted() ? ' - PROMOTED!' : ''
        );
    }
    

    Btw You should rename the AlbumTrackReference (for example "AlbumTrack"). It is clearly not only a reference, but contains additional logic. Since there are probably also Tracks that are not connected to an album but just available through a promo-cd or something this allows for a cleaner separation also.

    0 讨论(0)
  • 2020-11-22 11:30

    I think I would go with @beberlei's suggestion of using proxy methods. What you can do to make this process simpler is to define two interfaces:

    interface AlbumInterface {
        public function getAlbumTitle();
        public function getTracklist();
    }
    
    interface TrackInterface {
        public function getTrackTitle();
        public function getTrackDuration();
    }
    

    Then, both your Album and your Track can implement them, while the AlbumTrackReference can still implement both, as following:

    class Album implements AlbumInterface {
        // implementation
    }
    
    class Track implements TrackInterface {
        // implementation
    }
    
    /** @Entity whatever */
    class AlbumTrackReference implements AlbumInterface, TrackInterface
    {
        public function getTrackTitle()
        {
            return $this->track->getTrackTitle();
        }
    
        public function getTrackDuration()
        {
            return $this->track->getTrackDuration();
        }
    
        public function getAlbumTitle()
        {
            return $this->album->getAlbumTitle();
        }
    
        public function getTrackList()
        {
            return $this->album->getTrackList();
        }
    }
    

    This way, by removing your logic that is directly referencing a Track or an Album, and just replacing it so that it uses a TrackInterface or AlbumInterface, you get to use your AlbumTrackReference in any possible case. What you will need is to differentiate the methods between the interfaces a bit.

    This won't differentiate the DQL nor the Repository logic, but your services will just ignore the fact that you're passing an Album or an AlbumTrackReference, or a Track or an AlbumTrackReference because you've hidden everything behind an interface :)

    Hope this helps!

    0 讨论(0)
提交回复
热议问题