我想知道在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
对象 . 我无法创建代理方法,如果两者都有, Album
和 Track
会有 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 回答
我在Doctrine用户邮件列表中打开了一个类似的问题,得到了一个非常简单的答案;
将多对多关系视为一个实体本身,然后你意识到你有三个对象,它们之间以一对多和多对一的关系相互关联 .
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
一旦关系有数据,它就不再是关系了!
从$ album-> getTrackList()您将获得“AlbumTrackReference”实体,那么从Track和代理添加方法呢?
通过这种方式,您的循环以及与循环专辑曲目相关的所有其他代码都会大大简化,因为所有方法都只是在AlbumTrakcReference中代理:
顺便说一下你应该重命名AlbumTrackReference(例如“AlbumTrack”) . 它显然不仅是一个参考,还包含其他逻辑 . 由于可能还有轨道没有连接到专辑但只能通过促销CD或其他东西,这也允许更清晰的分离 .
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
我想我会选择@beberlei使用代理方法的建议 . 你可以做些什么来简化这个过程是定义两个接口:
然后,
Album
和Track
都可以实现它们,而AlbumTrackReference
仍然可以实现它们,如下所示:这样,通过删除直接引用
Track
或Album
的逻辑,只需将其替换为使用TrackInterface
或AlbumInterface
,就可以在任何可能的情况下使用AlbumTrackReference
. 您需要的是稍微区分接口之间的方法 .这赢得't differentiate the DQL nor the Repository logic, but your services will just ignore the fact that you'传递
Album
或AlbumTrackReference
,或Track
或AlbumTrackReference
,因为你隐藏了界面背后的一切:)希望这可以帮助!
首先,我主要同意贝伯莱的建议 . 但是,您可能正在将自己设计成陷阱 . 您的域似乎正在考虑将 Headers 作为曲目的自然键,这可能是您遇到的99%场景的情况 . 但是,如果木偶大师上的电池是与金属制品系列上的版本不同的版本(不同的长度,现场,声学,混音,重新制作等) .
根据你想要处理(或忽略)这种情况的方式,你可以去beberlei的建议路线,或者只是在Album :: getTracklist()中使用你提出的额外逻辑 . 就个人而言,我认为额外的逻辑是合理的,以保持您的API清洁,但两者都有其优点 .
如果你确实希望适应我的用例,你可以让Tracks包含一个自我引用OneToMany到其他Tracks,可能是$ similarTracks . 在这种情况下,轨道电池将有两个实体,一个用于The Metallica Collection,另一个用于Master of the Puppets . 然后每个类似的Track实体将包含对彼此的引用 . 此外,这将摆脱当前的AlbumTrackReference类并消除您当前的"issue" . 我同意它只是将复杂性转移到另一个点,但它能够处理以前无法实现的用例 .
你要求“最好的方式”,但没有最好的方法 . 有很多方法,你已经发现了其中一些 . 在使用关联类时,如何管理和/或封装关联管理完全取决于您和您的具体领域,没有人能够向您展示我害怕的“最佳方式” .
除此之外,通过从等式中删除Doctrine和关系数据库,可以大大简化这个问题 . 你的问题的本质归结为一个关于如何处理普通OOP中的关联类的问题 .
我从与关联类(具有其他自定义字段)注释中定义的连接表和在多对多注释中定义的连接表的冲突中获得 .
具有直接多对多关系的两个实体中的映射定义似乎导致使用“joinTable”注释自动创建连接表 . 但是,连接表已经在其底层实体类中由注释定义,我希望它使用此关联实体类自己的字段定义,以便使用其他自定义字段扩展连接表 .
解释和解决方案是由上面的FMaz008确定的 . 在我的情况下,这要归功于论坛'Doctrine Annotation Question'中的这篇文章 . 这篇文章提请注意关于ManyToMany Uni-directional relationships的Doctrine文档 . 查看关于使用'association entity class'的方法的注释,从而在主实体类中使用一对多注释和关联实体类中的两个'many-to-one'注释直接替换两个主实体类之间的多对多注释映射 . 此论坛帖子中提供了一个示例Association models with extra fields:
这个非常有用的例子 . 它缺乏文献学说2 .
非常感谢 .
对于代理,可以完成以下功能:
和
您所指的是元数据,有关数据的数据 . 对于我目前正在进行的项目,我遇到了同样的问题,并且不得不花一些时间来解决这个问题 . 这里发布的信息太多了,但下面有两个你可能会觉得有用的链接 . 他们确实参考了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参考!
解决方案在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评论:
这很简单,你只需做这样的事情:
所以你创建了一个实体ArticleTag
我希望它有所帮助
单向 . 只需添加inversedBy :(外部列名称)即可使其成为双向的 .
我希望它有所帮助 . 再见 .
您可以使用Class Table Inheritance将AlbumTrackReference更改为AlbumTrack,从而实现您想要的效果:
并且
getTrackList()
将包含AlbumTrack
对象,您可以随意使用它们:您需要彻底检查这一点,以确保您不会受到性能影响 .
您当前的设置简单,高效且易于理解,即使某些语义与您并不相符 .
虽然所有专辑曲目都在专辑类中,但是由于代理方法的原因,你需要'll generate one more query for one more record. That' . 还有我的代码的另一个例子(参见主题的最后一篇文章):http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
有没有其他方法可以解决这个问题?单一加入不是更好的解决方案吗?
这是Doctrine2 Documentation中描述的解决方案