php Doctrine2:在引用表中使用额外列处理多对多的最佳方法
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3542243/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Doctrine2: Best way to handle many-to-many with extra columns in reference table
提问by Crozin
I'm wondering what's the best, the cleanest and the most simply way to work with many-to-many relations in Doctrine2.
我想知道在 Doctrine2 中处理多对多关系的最佳、最干净和最简单的方法是什么。
Let's assume that we've got an album like Master of Puppetsby Metallicawith several tracks. But please note the fact that one track might appears in more that one album, like Batteryby Metallicadoes - three albums are featuring this track.
假设我们有一张像Metallica 的Master of Puppets这样的专辑,其中有几首曲目。但请注意,一首曲目可能会出现在不止一张专辑中,就像Metallica 的Battery所做的那样 - 三张专辑都包含这首曲目。
So what I need is many-to-many relationship between albums and tracks, using third table with some additional columns (like position of the track in specified album). Actually I have to use, as Doctrine's documentation suggests, a double one-to-many relation to achieve that functionality.
所以我需要的是专辑和曲目之间的多对多关系,使用带有一些附加列的第三个表(例如指定专辑中曲目的位置)。实际上,正如 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;
}
}
Sample data:
样本数据:
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 |
+----+----------+----------+----------+------------+
Now I can display a list of albums and tracks associated to them:
现在我可以显示与它们相关联的专辑和曲目列表:
$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 results are what I'm expecting, ie: a list of albums with their tracks in appropriate order and promoted ones being marked as 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)
So what's wrong?
那么怎么了?
This code demonstrates what's wrong:
此代码演示了错误所在:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
returns an array of AlbumTrackReference
objects instead of Track
objects. I can't create proxy methods cause what if both, Album
and Track
would have getTitle()
method? I could do some extra processing within Album::getTracklist()
method but what's the most simply way to do that? Am I forced do write something like that?
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
EDIT
编辑
@beberlei suggested to use proxy methods:
@beberlei 建议使用代理方法:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
That would be a good idea but I'm using that "reference object" from both sides: $album->getTracklist()[12]->getTitle()
and $track->getAlbums()[1]->getTitle()
, so getTitle()
method should return different data based on the context of invocation.
这将是一个好主意,但我正在使用双方的“引用对象”:$album->getTracklist()[12]->getTitle()
and $track->getAlbums()[1]->getTitle()
,因此getTitle()
方法应根据调用上下文返回不同的数据。
I would have to do something like:
我将不得不做这样的事情:
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();
}
And that's not a very clean way.
这不是一个非常干净的方式。
采纳答案by FMaz008
I've opened a similar question in the Doctrine user mailing list and got a really simple answer;
我在 Doctrine 用户邮件列表中打开了一个类似的问题,并得到了一个非常简单的答案;
consider the many to many relation as an entity itself, and then you realize you have 3 objects, linked between them with a one-to-many and many-to-one relation.
将多对多关系视为一个实体本身,然后您会意识到您有 3 个对象,它们之间通过一对多和多对一关系链接。
Once a relation has data, it's no more a relation !
一旦关系有了数据,它就不再是关系了!
回答by beberlei
From $album->getTrackList() you will alwas get "AlbumTrackReference" entities back, so what about adding methods from the Track and proxy?
从 $album->getTrackList() 中,您将始终获得“AlbumTrackReference”实体,那么从 Track 和代理添加方法呢?
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:
通过这种方式,您的循环以及与循环专辑曲目相关的所有其他代码都大大简化了,因为所有方法都只是在 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.
顺便说一句,您应该重命名 AlbumTrackReference(例如“AlbumTrack”)。它显然不仅是一个参考,而且包含了额外的逻辑。由于可能还有一些曲目没有连接到专辑,而只能通过促销 CD 或其他东西获得,这也允许更清晰的分离。
回答by Wilt
Nothing beats a nice example
没有什么能比得上一个很好的例子
For people looking for a clean coding example of an one-to-many/many-to-one associations between the 3 participating classes to store extra attributes in the relation check this site out:
对于正在寻找 3 个参与类之间的一对多/多对一关联的干净编码示例以在关系中存储额外属性的人,请查看此站点:
nice example of one-to-many/many-to-one associations between the 3 participating classes
Think about your primary keys
想想你的主键
Also think about your primary key. You can often use composite keys for relationships like this. Doctrine natively supports this. You can make your referenced entities into ids. Check the documentation on composite keys here
还要考虑您的主键。您通常可以将复合键用于此类关系。Doctrine 本身就支持这一点。您可以将引用的实体转换为 id。 在此处查看有关复合键的文档
回答by Ocramius
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:
我想我会同意@beberlei 的使用代理方法的建议。为了简化这个过程,你可以做的是定义两个接口:
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:
然后,你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();
}
}
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.
这样,通过删除直接引用 aTrack
或 an 的逻辑Album
,并仅替换它以便它使用 a TrackInterface
or AlbumInterface
,您就可以AlbumTrackReference
在任何可能的情况下使用您的。您需要的是稍微区分接口之间的方法。
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 :)
这不会区分 DQL 和存储库逻辑,但您的服务将忽略您传递 anAlbum
或 an AlbumTrackReference
,或 aTrack
或 an的事实,AlbumTrackReference
因为您已将所有内容隐藏在接口后面:)
Hope this helps!
希望这可以帮助!
回答by jsuggs
First, I mostly agree with beberlei on his suggestions. However, you may be designing yourself into a trap. Your domain appears to be considering the title to be the natural key for a track, which is likely the case for 99% of the scenarios you come across. However, what if Batteryon Master of the Puppetsis a different version (different length, live, acoustic, remix, remastered, etc) than the version on The Metallica Collection.
首先,我基本同意 beberlei 的建议。但是,您可能正在将自己设计成一个陷阱。您的域似乎将标题视为曲目的自然键,您遇到的 99% 的情况都可能是这种情况。但是,如果The Master of the Puppets上的Battery是与The Metallica Collection上的版本不同的版本(不同的长度、现场、原声、混音、重新制作等)怎么办。
Depending on how you want to handle (or ignore) that case, you could either go beberlei's suggested route, or just go with your proposed extra logic in Album::getTracklist(). Personally, I think the extra logic is justified to keep your API clean, but both have their merit.
根据您想如何处理(或忽略)这种情况,您可以选择 beberlei 建议的路线,也可以在 Album::getTracklist() 中使用您提议的额外逻辑。就我个人而言,我认为额外的逻辑对于保持 API 干净是合理的,但两者都有其优点。
If you do wish to accommodate my use case, you could have Tracks contain a self referencing OneToMany to other Tracks, possibly $similarTracks. In this case, there would be two entities for the track Battery, one for The Metallica Collectionand one for Master of the Puppets. Then each similar Track entity would contain a reference to each other. Also, that would get rid of the current AlbumTrackReference class and eliminate your current "issue". I do agree that it is just moving the complexity to a different point, but it is able to handle a usecase it wasn't previously able to.
如果您确实希望适应我的用例,您可以让 Tracks 包含一个自引用 OneToMany 到其他 Tracks,可能是 $similarTracks。在这种情况下,轨道Battery将有两个实体,一个用于The Metallica Collection,另一个用于Master of the Puppets。然后每个相似的 Track 实体将包含对彼此的引用。此外,这将摆脱当前的 AlbumTrackReference 类并消除您当前的“问题”。我确实同意它只是将复杂性转移到了不同的点,但它能够处理以前无法处理的用例。
回答by romanb
You ask for the "best way" but there is no best way. There are many ways and you already discovered some of them. How you want to manage and/or encapsulate association management when using association classes is entirely up to you and your concrete domain, noone can show you a "best way" I'm afraid.
你要求“最好的方法”,但没有最好的方法。有很多方法,你已经发现了其中的一些。在使用关联类时如何管理和/或封装关联管理完全取决于您和您的具体领域,恐怕没有人可以向您展示“最佳方式”。
Apart from that, the question could be simplified a lot by removing Doctrine and relational databases from the equation. The essence of your question boils down to a question about how to deal with association classes in plain OOP.
除此之外,通过从等式中删除 Doctrine 和关系数据库,可以大大简化问题。你的问题的本质归结为一个关于如何在普通 OOP 中处理关联类的问题。
回答by Onshop
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.
具有直接多对多关系的两个实体中的映射定义似乎导致使用“joinTable”注释自动创建连接表。但是,连接表已经由其底层实体类中的注释定义,我希望它使用此关联实体类自己的字段定义,以便使用其他自定义字段扩展连接表。
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:
解释和解决办法就是上面FMaz008所标识的。在我的情况下,这要归功于论坛“教义注释问题”中的这篇帖子。这篇文章提请注意关于多对多单向关系的 Doctrine 文档。查看有关使用“关联实体类”的方法的注释,从而将两个主要实体类之间的多对多注释映射直接替换为主要实体类中的一对多注释和两个“多对多”注释。 -one' 关联实体类中的注释。本论坛帖子中提供了一个带有额外字段的关联模型示例:
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;
}
回答by Mirza Selimovic
The solution is in the documentation of Doctrine. In the FAQ you can see this :
解决方案在 Doctrine 的文档中。在常见问题解答中,您可以看到:
And the tutorial is here :
教程在这里:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
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.
所以你不再做 amanyToMany
但你必须创建一个额外的实体并放入manyToOne
你的两个实体。
ADDfor @f00bar comment :
添加@f00bar 评论:
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
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
我希望它有帮助
回答by Gatunox
Unidirectional. Just add the inversedBy:(Foreign Column Name) to make it Bidirectional.
单向。只需添加 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
I hope it helps. See you.
我希望它有帮助。再见。
回答by Mike Purcell
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.
你所指的是元数据,关于数据的数据。我目前正在从事的项目也遇到了同样的问题,不得不花一些时间来弄清楚。在这里发布的信息太多,但下面是两个您可能会觉得有用的链接。它们确实参考了 Symfony 框架,但基于 Doctrine ORM。
http://melikedev.com/2010/04/06/symfony-saving-metadata-during-form-save-sort-ids/
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/
http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/
Good luck, and nice Metallica references!
祝你好运,还有很好的 Metallica 参考!