Doctrine2:在参考表中使用额外的列处理多一对多列的最佳方法
-
30-09-2019 - |
题
我想知道什么是最好的,最干净,最简单的方式与Doctrine2中的多一对一关系合作。
假设我们有一张专辑 木偶大师 由Metallica 有几首曲目。但是请注意一个事实,一个曲目可能会出现在更多专辑中,例如 电池 由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()
}
}
那将是一个好主意,但我正在使用双方的“参考对象”: $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();
}
这不是一种非常干净的方法。
解决方案
我在学说用户邮件列表中开了一个类似的问题,并得到了一个非常简单的答案。
将许多与许多关系视为实体本身,然后您意识到自己有3个对象,它们之间与一对一和多一对一的关系链接在一起。
一旦关系拥有数据,就不再是关系!
其他提示
从$ - 专辑 - > getTrackList()您会返回“ profertrackReference”实体,那么从轨道和代理中添加方法又如何呢?
class AlbumTrackReference
{
public function getTitle()
{
return $this->getTrack()->getTitle();
}
public function getDuration()
{
return $this->getTrack()->getDuration();
}
}
这样,您的循环大大简化了与循环专辑的曲目相关的所有其他代码,因为所有方法都在problowtrakcreference中代理:
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!' : ''
);
}
顺便说一句,您应该重命名曲线曲线(例如“曲线”)。显然,它不仅是参考,还包含其他逻辑。由于可能也有没有连接到专辑的曲目,但仅通过促销-CD或其他内容提供,也可以进行清洁的分离。
没有什么比一个很好的例子
对于寻找一个干净的编码示例的人来说
考虑一下您的主要钥匙
还要考虑您的主要钥匙。您通常可以将复合密钥用于这样的关系。学说本地支持这一点。您可以将您的引用实体变成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
因为您已经隐藏了界面后面的所有内容:)
希望这可以帮助!
首先,我主要同意Beberlei的建议。但是,您可能正在将自己设计成陷阱。您的域似乎将标题视为曲目的自然钥匙,而您遇到的99%的场景可能就是这种情况。但是,如果 电池 在 木偶大师 是一个不同的版本(长度不同,现场,声学,混音,重新制作等) Metallica系列.
根据您要处理(或忽略)这种情况的方式,您可以走Beberlei的建议路线,或者只使用专辑:: getTrackList()中建议的额外逻辑。就我个人而言,我认为额外的逻辑是合理的,可以使您的API保持清洁,但两者都有其优点。
如果您确实希望容纳我的用例,则可以有一个曲目包含一个自我引用到其他轨道的轨道,可能是$类似的轨道。在这种情况下,轨道将有两个实体 电池, ,一个 Metallica系列 还有一个 木偶大师. 。然后,每个类似的轨道实体将互相引用。另外,这将摆脱当前的曲线式奖学金,并消除您当前的“问题”。我确实同意它只是将复杂性移至另一点,但是它能够处理以前无法做到的用户酶。
您要求“最佳方式”,但没有最好的方法。有很多方法,您已经发现了其中一些。当使用协会课程时,您要如何管理和/或封装协会管理完全取决于您和您的具体域,恐怕我可以向您展示一种“最佳方式”。
除此之外,可以通过从方程式中删除学说和关系数据库来大量简化这个问题。您的问题的本质归结为一个关于如何在普通OOP中处理协会课程的问题。
我从与关联表中定义的加入表(带有其他自定义字段)的注释中定义的加入表的冲突,并在多对多注释中定义了一个联接表。
具有直接多次关系的两个实体中的映射定义似乎导致使用“可共同”注释自动创建联接表。但是,加入表已经通过其基础实体类中的注释来定义,我希望它使用该协会实体类自己的字段定义,以扩展加入表和其他自定义字段。
解释和解决方案是上面FMAZ008确定的。在我的情况下,这要归功于论坛上的这篇文章学说注释问题'。这篇文章提请人们注意有关学说的文档 Manytomany单向关系. 。查看有关使用“协会实体类”方法的注释,从而在两个主要实体类中直接替换了两个主要实体类之间的多对多注释映射,并在主要实体类中进行一对多注释和两个“ - 协会实体类中的一个注释。该论坛帖子中提供了一个示例 与额外字段的关联模型:
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;
}
这个非常有用的例子。它缺乏文档学说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框架,但基于学说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-realations/
祝你好运,也很好的metallica参考!
该解决方案是在学说的文档中。在常见问题解答中,您可以看到:
教程在这里:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
所以你不再做 manyToMany
但是您必须创建一个额外的实体并放置 manyToOne
给你的两个实体。
添加 @f00bar评论:
很简单,您只需要做这样的事情:
Article 1--N ArticleTag N--1 Tag
因此,您创建了一个实体工件
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
我希望它有帮助
单向。只需添加倒置:(外列名称)即可使其双向。
# 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
我希望它有帮助。再见。
您也许可以实现自己想要的 类表继承 您在哪里将profertrackreference更改为蛋白质:
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!' : ''
);
}
您将需要仔细检查一下,以确保您不会遭受性能的痛苦。
您当前的设置简单,高效且易于理解,即使某些语义并不完全与您同在。
在专辑类中获得所有专辑曲目的表单时,您将再产生一个查询,以获得另一张唱片。那是因为代理方法。我的代码还有另一个示例(请参阅主题中的最后一篇文章): http://groups.google.com/group/doctrine-user/browse_thread/thread/thread/d1d87c96052e76f7/436B896E83C1086C10868#436B896E896E83C10868
还有其他方法可以解决吗?单一加入更好的解决方案吗?
这是如前所述的解决方案 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();
}
}