Doctrine2:在引用表中处理带有额外列的多对多关系的最佳方法

294
我想知道在Doctrine2中处理多对多关系的最佳、最干净和最简单的方法是什么。
假设我们有一张专辑像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 对象。我不能创建代理方法,因为如果 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();
 }

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


2
你如何处理AlbumTrackReference?例如$album->addTrack()或$album->removeTrack()? - Daniel
我不理解你关于上下文的评论。在我看来,数据不依赖于上下文。关于$album->getTracklist()[12]AlbumTrackRef对象,所以$album->getTracklist()[12]->getTitle()将始终返回曲目的标题(如果您使用代理方法)。而$track->getAlbums()[1]Album对象,所以$track->getAlbums()[1]->getTitle()将始终返回专辑的标题。 - Vinícius Fagundes
另一个想法是在 AlbumTrackReference 上使用两个代理方法,getTrackTitle()getAlbumTitle - Vinícius Fagundes
13个回答

2
您可以使用类表继承来实现您想要的效果,将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!' : ''
    );
}

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

您当前的设置是简单、高效和易于理解的,即使有些语义不太符合您的要求。


0

这里是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();
    }
}

0

1
虽然这理论上回答了问题,但最好在此处包含答案的基本部分,并提供参考链接。 - Spontifixus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接