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

我想知道在Doctrine2中处理多对多关系最好、最干净、最简单的方法是什么。

让我们假设我们有一张像木偶大师由Metallica这样的专辑,里面有几首歌。但请注意,这首歌可能会出现在不止一张专辑中,就像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对象数组。我不能创建代理方法,因为如果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()
}
}

这将是一个好主意,但我从两边使用“引用对象”:$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();
}

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

111138 次浏览

从$album->getTrackList()你会得到“AlbumTrackReference”实体回来,那么从轨道和代理添加方法呢?

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”)。它显然不仅是一个引用,而且还包含额外的逻辑。因为也有可能是没有连接到专辑,但只是通过促销光盘或其他东西,这也允许一个更清晰的分离。

你要求“最好的方法”,但没有最好的方法。有很多方法,其中一些你已经发现了。在使用关联类时,你想要如何管理和/或封装关联管理完全取决于你和你的具体领域,恐怕没有人能告诉你一个“最好的方法”。

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

你可以用类表继承实现你想要的,其中你将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!' : ''
);
}

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

您当前的设置简单、高效且易于理解,即使有些语义不太适合您。

首先,我基本同意beberlei的建议。然而,你可能把自己设计进了陷阱。您的域名似乎认为标题是音轨的自然键,这可能是您遇到的99%的场景的情况。然而,如果木偶的主人上的电池金属乐队收藏上的版本(不同的长度、现场、原声、混音、重制等)不同呢?

根据您想要如何处理(或忽略)这种情况,您可以选择beberlei建议的路线,或者只是使用Album::getTracklist()中建议的额外逻辑。就我个人而言,我认为额外的逻辑是合理的,以保持API的简洁,但两者都有各自的优点。

如果您确实希望适应我的用例,您可以让Tracks包含一个自引用OneToMany到其他Tracks,可能是$similarTracks。在这种情况下,轨道电池将有两个实体,一个用于金属乐队收藏,另一个用于木偶的主人。然后,每个类似的Track实体将包含对彼此的引用。同样,这将摆脱当前的AlbumTrackReference类并消除当前的“问题”。我同意它只是将复杂性转移到一个不同的点,但它能够处理以前无法处理的用例。

这个例子很有用。它缺乏文件主义。

非常感谢。

对于代理功能可以做:

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;

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

将多对多关系视为一个实体本身,然后您会意识到您有3个对象,它们之间通过一对多和多对一的关系进行链接。

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

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

当从专辑类中获取所有专辑曲目时,您将为另一条记录生成另一个查询。这是因为代理方法。还有另一个我的代码示例(参见主题的上一篇文章):http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

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

我想我会同意@beberlei使用代理方法的建议。为了让这个过程更简单,你可以定义两个接口:

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的逻辑,并将其替换为使用TrackInterfaceAlbumInterface,您可以在任何可能的情况下使用AlbumTrackReference。您需要做的是稍微区分这些接口之间的方法。

这不会区分DQL和Repository逻辑,但你的服务会忽略你正在传递AlbumAlbumTrackReference,或TrackAlbumTrackReference的事实,因为你把所有东西都隐藏在接口后面:)

希望这能有所帮助!

没有什么比一个好的例子更好的了

对于想要在3个参与类之间存储额外属性的一对多/多对一关联的简洁代码示例的人,请查看这个网站:

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

考虑一下主键

还要考虑你的主键。对于这样的关系,通常可以使用复合键。教义本身就支持这一点。您可以将引用的实体转换为id。 在这里查看关于复合键的文档 < / p >

我从一个关联类(带有额外的自定义字段)注释中定义的连接表和一个多对多注释中定义的连接表的冲突中得到。

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

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

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;


}

解决方案在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,但是你必须创建一个额外的Entity,并把manyToOne放到你的两个实体中。

添加用于@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

我希望这对你们有帮助

单向。只需添加反向的by:(外列名),使其成为双向的。

# 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
我希望这对你有帮助。 见到你。< / p >

下面是Doctrine2文档中描述的解决方案

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