Doctrine2:在参考表中用多余的列来处理多对多的最佳方式

我想知道什么是最好的,最简单的方法来处理Doctrine2中的多对多关系。

假设我们已经有了Metallica 的木偶大师Master of Puppets)等几张专辑。 但是请注意一个曲目可能会出现在更多的专辑中,比如Metallica的Battery – 三张专辑都以这个曲目为特色。

所以我需要的是专辑和曲目之间的多对多关系,使用第三个表格和一些额外的列(比如指定专辑中曲目的位置)。 其实我必须使用,正如Doctrine的文档所build议的那样,要实现这个function,必须使用双重一对多的关系。

/** @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对象的数组。 我不能创build代理方法会导致如果两个, 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 

编辑

@beberleibuild议使用代理方法:

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

这将是一个好主意,但我用双方的“参考对象”: $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(); } 

这不是一个很干净的方法。

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

把多对多的关系看成一个实体本身,然后你意识到你有三个对象,它们之间有一对多和多对一的关系。

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

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

从$ 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或东西,这也允许更清洁的分离。

没有什么比一个很好的例子

对于寻找3个参与类之间的一对多/多对一关联的干净编码示例的人来在关系中存储额外的属性,请检查这个网站:

3个参与类之间的一对多/多对一关联的很好的例子

考虑一下你的主键

也想想你的主要关键。 你可以经常使用组合键来处理这种关系。 学说原生支持这一点。 你可以使你的引用实体成为ID。 在这里检查组合键的文档

我想我会与@ beberlei的使用代理方法的build议。 你可以做些简单的事情来定义两个接口:

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

然后,您的Album和您的Track都可以实现它们,而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逻辑,并将其replace为使用TrackInterfaceAlbumInterface ,就可以在任何情况下使用您的AlbumTrackReference 。 你将需要的是有点区分接口之间的方法。

这不会区分DQL和Repository逻辑,但是您的服务将忽略事实,即您传递了一个Album或一个AlbumTrackReference ,或者一个Track或一个AlbumTrackReference因为您已经将所有内容都隐藏在界面后面了:)

希望这可以帮助!

首先,我大多同意贝伯雷的build议。 不过,你可能正在把自己devise成陷阱。 您的域似乎正在考虑标题是一个轨道的自然关键,这可能是99%的情况下你遇到的情况。 然而,如果木偶大师的 电池The Metallica Collection的版本不同,则会有什么不同的版本(不同的长度,生活,原声,混音,重新制作等)。

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

如果你希望容纳我的用例,你可以让Tracks包含一个自引用OneToMany到其他轨道,可能是$ similarTracks。 在这种情况下, 电池轨道将有两个实体,一个用于The Metallica Collection ,一个用于木偶大师 。 然后,每个类似的Track实体将包含对彼此的引用。 此外,这将摆脱目前的AlbumTrackReference类,并消除您当前的“问题”。 我同意,只是将复杂性转移到不同的地方,但它能够处理以前无法使用的用例。

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

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

我从与在关联类(具有附加的自定义字段)注释中定义的连接表冲突中获得,并且在多对多注释中定义了连接表。

具有直接多对多关系的两个实体中的映射定义似乎导致使用“joinTable”注释自动创build连接表。 然而,连接表已经由其基础实体类中的注释定义,我希望它使用这个关联实体类自己的字段定义,以扩展连接表与额外的自定义字段。

解释和解决办法是由FMaz008确定的。 在我的情况下,感谢这个post在论坛的“ 主义诠释问题 ”。 这篇文章提请注意关于ManyToMany单向关系的Doctrine文档。 查看关于使用“关联实体类”的方法的说明,从而用主实体类中的一对多注释和两个“多对多”直接replace两个主要实体类之间的多对多注释映射关联实体类中的“一个”注释。 这个论坛有一个例子提供了一个带有额外字段的关联模型 :

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

这个非常有用的例子。 它缺less文档的教条2。

非常感谢。

代理function可以做到:

 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; 

你所指的是元数据,关于数据的数据。 对于我目前正在进行的项目,我也有同样的问题,不得不花一些时间来解决这个问题。 这里发布的信息太多,但以下两条链接可能会对您有所帮助。 他们引用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参考!

该解决scheme在Doctrine的文档中。 在FAQ中你可以看到这个:

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但你必须创build一个额外的实体,并把manyToOne放到你的两个实体中。

添加 @ f00bar评论:

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

 Article 1--N ArticleTag N--1 Tag 

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

我希望它有帮助

单向。 只需添加inversedBy:(Foreign Column Name)使其成为双向。

 # 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 

我希望它有帮助。 再见。

您可以使用Class Table Inheritance来实现您想要的function,将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!' : '' ); } 

您将需要彻底检查以确保您不会受到性能方面的影响。

即使某些语义不完全与您坐在一起,您目前的设置也很简单,高效且易于理解。

在相册类中获取所有专辑曲目的同时,您将再生成一个查询以获得更多的唱片。 那是因为代理方法。 我的代码还有另外一个例子(请参阅主题的最后一篇文章): http : //groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

有没有其他方法可以解决这个问题? 是不是一个更好的解决scheme?

这是Doctrine2文档中描述的解决scheme

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