[英]Doctrine2: Best way to handle many-to-many with extra columns in reference table
我想知道在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()
方法中进行一些额外的处理,但是最简单的方法是什么? 我是否被迫写类似的东西?
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();
}
那不是一个很干净的方法。
我已经在“ Doctrine用户”邮件列表中打开了一个类似的问题,并且得到了一个非常简单的答案。
将多对多关系视为一个实体本身,然后您意识到您有3个对象,它们之间以一对多和多对一关系链接。
关系一旦有了数据,就不再是关系!
从$ 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”)。 显然,它不仅是参考,还包含其他逻辑。 由于可能还存在一些未连接到专辑的音轨,只能通过promo-cd或其他东西使用,因此也可以使音轨更清晰。
没有比这更好的例子了
对于正在寻找3个参与类之间一对多/多对一关联的干净编码示例以在关系中存储额外属性的人们,请访问以下站点:
考虑一下您的主键
还考虑您的主键。 您通常可以将复合键用于此类关系。 学说本身就支持这一点。 您可以将引用的实体设置为ID。 在此处查看有关组合键的文档
我想我会接受@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();
}
}
这样,通过删除直接引用Track
或Album
逻辑,并仅替换它以使其使用TrackInterface
或AlbumInterface
,就可以在任何可能的情况下使用AlbumTrackReference
。 您需要的是稍微区分接口之间的方法。
这不会区分DQL还是存储库逻辑,但是您的服务将仅忽略您传递Album
或AlbumTrackReference
, Track
或AlbumTrackReference
因为您已将所有内容隐藏在接口后面:)
希望这可以帮助!
首先,对于伯贝雷的建议,我大都同意。 但是,您可能正在将自己设计为陷阱。 您的域似乎正在考虑将标题作为曲目的自然键,对于您所遇到的99%的情况来说,情况很可能如此。 但是,如果“木偶大师”上的 电池与The Metallica Collection上的版本不同(不同的长度,现场,原声,混音,重新制作等),该怎么办。
根据您要处理(或忽略)该情况的方式,您可以采用beberlei的建议路线,也可以仅使用Album :: getTracklist()中建议的额外逻辑。 就我个人而言,我认为可以使用额外的逻辑来保持API的整洁,但是两者都有其优点。
如果您确实希望容纳我的用例,则可以让Track包含一个将OneToMany自我引用到其他Track(可能是$ similarTracks)的自引用。 在这种情况下, 电池轨道将有两个实体,一个是The Metallica Collection的实体,一个是Puppets的主人 。 然后,每个类似的Track实体将包含彼此的引用。 另外,这将摆脱当前的AlbumTrackReference类,并消除当前的“问题”。 我确实同意,这只是将复杂性转移到了另一点,但是它能够处理以前无法实现的用例。
我从与关联类(具有其他自定义字段)注释中定义的联接表和多对多注释中定义的联接表的冲突中摆脱出来。
具有直接多对多关系的两个实体中的映射定义似乎导致使用“ joinTable”注释自动创建联接表。 但是,联接表已经由其基础实体类中的注释定义了,我希望它使用此关联实体类自己的字段定义,以便用其他自定义字段扩展联接表。
说明和解决方案由上面的FMaz008标识。 就我而言,这要归功于论坛“ 学说注释问题 ”中的这篇文章。 这篇文章引起人们对有关多对多单向关系的学说文档的关注。 请查看有关使用“关联实体类”的方法的注释,该方法因此将两个主要实体类之间的多对多注释映射直接替换为主要实体类中的一对多注释和两个“多个对多”注释关联实体类中的-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;
}
您要求“最佳方法”,但没有最佳方法。 有很多方法,您已经发现了其中一些方法。 使用关联类时如何管理和/或封装关联管理完全取决于您和您的具体领域,恐怕没人能向您展示“最佳方式”。
除此之外,通过从等式中删除主义和关系数据库,可以大大简化该问题。 您问题的实质可以归结为有关如何在普通OOP中处理关联类的问题。
解决方案在Doctrine的文档中。 在常见问题中,您可以看到:
教程在这里:
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
希望对您有所帮助
单向。 只需添加inversedBy :(外来列名称)使其成为双向的即可。
# 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
希望对您有所帮助。 再见。
这个例子非常有用。 它在文档学说2中缺乏。
非常感谢。
对于代理功能可以做到:
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!
您可以通过将TableTrackReference更改为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!' : ''
);
}
您将需要彻底检查这一点,以确保您不会遭受性能方面的困扰。
即使某些语义不太适合您,您当前的设置仍然简单,高效且易于理解。
这是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();
}
}
在专辑类中获取所有专辑曲目的形式时,您将为一个以上的记录生成另一个查询。 那是因为代理方法。 我的代码还有另一个示例(请参阅主题中的最后一篇文章): http : //groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
还有其他解决方法吗? 单一联接不是更好的解决方案吗?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.