我想知道在 Doctrine2 中处理多对多关系的最好、最干净和最简单的方法是什么。
假设我们有一张像Metallica的 Master of Puppets 这样的专辑,里面有几首曲目。但请注意,一首曲目可能会出现在一张专辑中,就像Metallica 的 Battery 一样 - 三张专辑都包含这首曲目。
所以我需要的是专辑和曲目之间的多对多关系,使用带有一些附加列的第三个表(如指定专辑中曲目的位置)。实际上,正如 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对象。我无法创建代理方法,如果两者兼而有之怎么办,Album并且Track会有getTitle()方法?我可以在方法中做一些额外的处理,Album::getTracklist()但最简单的方法是什么?我是被迫写这样的东西吗?
Album::getTracklist()
AlbumTrackReference
Track
Album
getTitle()
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() } }
这将是一个好主意,但我正在使用双方的“参考对象”:$album->getTracklist()[12]->getTitle()和$track->getAlbums()[1]->getTitle(),所以getTitle()方法应该根据调用的上下文返回不同的数据。
$album->getTracklist()[12]->getTitle()
$track->getAlbums()[1]->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 用户邮件列表中打开了一个类似的问题,得到了一个非常简单的答案;
将多对多关系视为一个实体本身,然后您会意识到您有 3 个对象,它们之间以一对多和多对一的关系链接。
http://groups.google.com/group/doctrine- user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
一旦关系有了数据,它就不再是关系了!