首页 文章

Doctrine2:在参考表中使用额外列处理多对多的最佳方法

提问于
浏览
269

我想知道在Doctrine2中使用多对多关系的最好,最干净,最简单的方法是什么 .

让's assume that we'有一个像Master of Puppets by Metallica这样的专辑有几首曲目 . 但请注意,一首曲目可能出现在一张专辑中,例如Battery by Metallica,三张专辑正在播放这首专辑 .

所以我需要的是专辑和曲目之间的多对多关系,使用第三个表和一些额外的列(比如指定专辑中曲目的位置) . 实际上,我必须使用,如Doctrine的文档所示,实现该功能的双重一对多关系 .

/** @Entity() */
class Album {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="album") */
    protected $tracklist;

    public function __construct() {
        $this->tracklist = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getTitle() {
        return $this->title;
    }

    public function getTracklist() {
        return $this->tracklist->toArray();
    }
}

/** @Entity() */
class Track {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @Column(type="time") */
    protected $duration;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="track") */
    protected $albumsFeaturingThisTrack; // btw: any idea how to name this relation? :)

    public function getTitle() {
        return $this->title;
    }

    public function getDuration() {
        return $this->duration;
    }
}

/** @Entity() */
class AlbumTrackReference {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @ManyToOne(targetEntity="Album", inversedBy="tracklist") */
    protected $album;

    /** @ManyToOne(targetEntity="Track", inversedBy="albumsFeaturingThisTrack") */
    protected $track;

    /** @Column(type="integer") */
    protected $position;

    /** @Column(type="boolean") */
    protected $isPromoted;

    public function getPosition() {
        return $this->position;
    }

    public function isPromoted() {
        return $this->isPromoted;
    }

    public function getAlbum() {
        return $this->album;
    }

    public function getTrack() {
        return $this->track;
    }
}

样本数据:

Album
+----+--------------------------+
| id | title                    |
+----+--------------------------+
|  1 | Master of Puppets        |
|  2 | The Metallica Collection |
+----+--------------------------+

               Track
+----+----------------------+----------+
| id | title                | duration |
+----+----------------------+----------+
|  1 | Battery              | 00:05:13 |
|  2 | Nothing Else Matters | 00:06:29 |
|  3 | Damage Inc.          | 00:05:33 |
+----+----------------------+----------+

              AlbumTrackReference
+----+----------+----------+----------+------------+
| id | album_id | track_id | position | isPromoted |
+----+----------+----------+----------+------------+
|  1 |        1 |        2 |        2 |          1 |
|  2 |        1 |        3 |        1 |          0 |
|  3 |        1 |        1 |        3 |          0 |
|  4 |        2 |        2 |        1 |          0 |
+----+----------+----------+----------+------------+

现在我可以显示与他们相关的专辑和曲目列表:

$dql = '
    SELECT   a, tl, t
    FROM     Entity\Album a
    JOIN     a.tracklist tl
    JOIN     tl.track t
    ORDER BY tl.position ASC
';

$albums = $em->createQuery($dql)->getResult();

foreach ($albums as $album) {
    echo $album->getTitle() . PHP_EOL;

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

结果是我所期待的,即:一个专辑列表,其中的曲目以适当的顺序排列,而促销的专辑标记为已晋升 .

The Metallica Collection
    #1 - Nothing Else Matters (00:06:29) 
Master of Puppets
    #1 - Damage Inc.          (00:05:33) 
    #2 - Nothing Else Matters (00:06:29)  - PROMOTED!
    #3 - Battery              (00:05:13)

那有什么不对?

这段代码演示了什么错误:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}

Album::getTracklist() 返回 AlbumTrackReference 对象的数组,而不是 Track 对象 . 我无法创建代理方法,如果两者都有, AlbumTrack 会有 getTitle() 方法?我可以在 Album::getTracklist() 方法中做一些额外的处理但是最简单的方法是什么?我是否被迫写了类似的东西?

public function getTracklist() {
    $tracklist = array();

    foreach ($this->tracklist as $key => $trackReference) {
        $tracklist[$key] = $trackReference->getTrack();

        $tracklist[$key]->setPosition($trackReference->getPosition());
        $tracklist[$key]->setPromoted($trackReference->isPromoted());
    }

    return $tracklist;
}

// And some extra getters/setters in Track class

编辑

@beberlei建议使用代理方法:

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

这将是一个好主意,但我正在使用双方的"reference object": $album->getTracklist()[12]->getTitle()$track->getAlbums()[1]->getTitle() ,因此 getTitle() 方法应该根据调用的上下文返回不同的数据 .

我必须做以下事情:

getTracklist() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ....

 getAlbums() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ...

 AlbumTrackRef::getTitle() {
      return $this->{$this->context}->getTitle();
 }

这不是一个非常干净的方式 .

14 回答

  • 0

    我在Doctrine用户邮件列表中打开了一个类似的问题,得到了一个非常简单的答案;

    将多对多关系视为一个实体本身,然后你意识到你有三个对象,它们之间以一对多和多对一的关系相互关联 .

    http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

    一旦关系有数据,它就不再是关系了!

  • 5

    从$ album-> getTrackList()您将获得“AlbumTrackReference”实体,那么从Track和代理添加方法呢?

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

    通过这种方式,您的循环以及与循环专辑曲目相关的所有其他代码都会大大简化,因为所有方法都只是在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!' : ''
        );
    }
    

    顺便说一下你应该重命名AlbumTrackReference(例如“AlbumTrack”) . 它显然不仅是一个参考,还包含其他逻辑 . 由于可能还有轨道没有连接到专辑但只能通过促销CD或其他东西,这也允许更清晰的分离 .

  • 13

    Nothing beats a nice example

    对于寻找3个参与类之间的一对多/多对一关联的清晰编码示例以在关系中存储额外属性的人,请检查此站点:

    nice example of one-to-many/many-to-one associations between the 3 participating classes

    Think about your primary keys

    还要想想你的主键 . 您可以经常使用复合键来实现这样的关系 . Doctrine原生支持这一点 . 您可以将引用的实体变为ID . Check the documentation on composite keys here

  • 6

    我想我会选择@beberlei使用代理方法的建议 . 你可以做些什么来简化这个过程是定义两个接口:

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

    然后, AlbumTrack 都可以实现它们,而 AlbumTrackReference 仍然可以实现它们,如下所示:

    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();
        }
    }
    

    这样,通过删除直接引用 TrackAlbum 的逻辑,只需将其替换为使用 TrackInterfaceAlbumInterface ,就可以在任何可能的情况下使用 AlbumTrackReference . 您需要的是稍微区分接口之间的方法 .

    这赢得't differentiate the DQL nor the Repository logic, but your services will just ignore the fact that you'传递 AlbumAlbumTrackReference ,或 TrackAlbumTrackReference ,因为你隐藏了界面背后的一切:)

    希望这可以帮助!

  • 3

    首先,我主要同意贝伯莱的建议 . 但是,您可能正在将自己设计成陷阱 . 您的域似乎正在考虑将 Headers 作为曲目的自然键,这可能是您遇到的99%场景的情况 . 但是,如果木偶大师上的电池是与金属制品系列上的版本不同的版本(不同的长度,现场,声学,混音,重新制作等) .

    根据你想要处理(或忽略)这种情况的方式,你可以去beberlei的建议路线,或者只是在Album :: getTracklist()中使用你提出的额外逻辑 . 就个人而言,我认为额外的逻辑是合理的,以保持您的API清洁,但两者都有其优点 .

    如果你确实希望适应我的用例,你可以让Tracks包含一个自我引用OneToMany到其他Tracks,可能是$ similarTracks . 在这种情况下,轨道电池将有两个实体,一个用于The Metallica Collection,另一个用于Master of the Puppets . 然后每个类似的Track实体将包含对彼此的引用 . 此外,这将摆脱当前的AlbumTrackReference类并消除您当前的"issue" . 我同意它只是将复杂性转移到另一个点,但它能够处理以前无法实现的用例 .

  • 17

    你要求“最好的方式”,但没有最好的方法 . 有很多方法,你已经发现了其中一些 . 在使用关联类时,如何管理和/或封装关联管理完全取决于您和您的具体领域,没有人能够向您展示我害怕的“最佳方式” .

    除此之外,通过从等式中删除Doctrine和关系数据库,可以大大简化这个问题 . 你的问题的本质归结为一个关于如何处理普通OOP中的关联类的问题 .

  • 9

    我从与关联类(具有其他自定义字段)注释中定义的连接表和在多对多注释中定义的连接表的冲突中获得 .

    具有直接多对多关系的两个实体中的映射定义似乎导致使用“joinTable”注释自动创建连接表 . 但是,连接表已经在其底层实体类中由注释定义,我希望它使用此关联实体类自己的字段定义,以便使用其他自定义字段扩展连接表 .

    解释和解决方案是由上面的FMaz008确定的 . 在我的情况下,这要归功于论坛'Doctrine Annotation Question'中的这篇文章 . 这篇文章提请注意关于ManyToMany Uni-directional relationships的Doctrine文档 . 查看关于使用'association entity class'的方法的注释,从而在主实体类中使用一对多注释和关联实体类中的两个'many-to-one'注释直接替换两个主实体类之间的多对多注释映射 . 此论坛帖子中提供了一个示例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;
    
    }
    
  • 7

    这个非常有用的例子 . 它缺乏文献学说2 .

    非常感谢 .

    对于代理,可以完成以下功能:

    class AlbumTrack extends AlbumTrackAbstract {
       ... proxy method.
       function getTitle() {} 
    }
    
    class TrackAlbum extends AlbumTrackAbstract {
       ... proxy method.
       function getTitle() {}
    }
    
    class AlbumTrackAbstract {
       private $id;
       ....
    }
    

    /** @OneToMany(targetEntity="TrackAlbum", mappedBy="album") */
    protected $tracklist;
    
    /** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
    protected $albumsFeaturingThisTrack;
    
  • 3

    您所指的是元数据,有关数据的数据 . 对于我目前正在进行的项目,我遇到了同样的问题,并且不得不花一些时间来解决这个问题 . 这里发布的信息太多了,但下面有两个你可能会觉得有用的链接 . 他们确实参考了Symfony框架,但是基于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/

    祝你好运,还有很好的Metallica参考!

  • 2

    解决方案在Doctrine的文档中 . 在常见问题解答中,您可以看到:

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

    教程在这里:

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

    所以你不再做 manyToMany 但是你必须创建一个额外的实体并将 manyToOne 放到你的两个实体中 .

    ADD 代表@ f00bar评论:

    这很简单,你只需做这样的事情:

    Article  1--N  ArticleTag  N--1  Tag
    

    所以你创建了一个实体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
    

    我希望它有所帮助

  • 3

    单向 . 只需添加inversedBy :(外部列名称)即可使其成为双向的 .

    # 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
    

    我希望它有所帮助 . 再见 .

  • 3

    您可以使用Class Table Inheritance将AlbumTrackReference更改为AlbumTrack,从而实现您想要的效果:

    class AlbumTrack extends Track { /* ... */ }
    

    并且 getTrackList() 将包含 AlbumTrack 对象,您可以随意使用它们:

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

    您需要彻底检查这一点,以确保您不会受到性能影响 .

    您当前的设置简单,高效且易于理解,即使某些语义与您并不相符 .

  • 153

    虽然所有专辑曲目都在专辑类中,但是由于代理方法的原因,你需要'll generate one more query for one more record. That' . 还有我的代码的另一个例子(参见主题的最后一篇文章):http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

    有没有其他方法可以解决这个问题?单一加入不是更好的解决方案吗?

  • 0

    这是Doctrine2 Documentation中描述的解决方案

    <?php
    use Doctrine\Common\Collections\ArrayCollection;
    
    /** @Entity */
    class Order
    {
        /** @Id @Column(type="integer") @GeneratedValue */
        private $id;
    
        /** @ManyToOne(targetEntity="Customer") */
        private $customer;
        /** @OneToMany(targetEntity="OrderItem", mappedBy="order") */
        private $items;
    
        /** @Column(type="boolean") */
        private $payed = false;
        /** @Column(type="boolean") */
        private $shipped = false;
        /** @Column(type="datetime") */
        private $created;
    
        public function __construct(Customer $customer)
        {
            $this->customer = $customer;
            $this->items = new ArrayCollection();
            $this->created = new \DateTime("now");
        }
    }
    
    /** @Entity */
    class Product
    {
        /** @Id @Column(type="integer") @GeneratedValue */
        private $id;
    
        /** @Column(type="string") */
        private $name;
    
        /** @Column(type="decimal") */
        private $currentPrice;
    
        public function getCurrentPrice()
        {
            return $this->currentPrice;
        }
    }
    
    /** @Entity */
    class OrderItem
    {
        /** @Id @ManyToOne(targetEntity="Order") */
        private $order;
    
        /** @Id @ManyToOne(targetEntity="Product") */
        private $product;
    
        /** @Column(type="integer") */
        private $amount = 1;
    
        /** @Column(type="decimal") */
        private $offeredPrice;
    
        public function __construct(Order $order, Product $product, $amount = 1)
        {
            $this->order = $order;
            $this->product = $product;
            $this->offeredPrice = $product->getCurrentPrice();
        }
    }
    

相关问题