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

Cro*_*zin 276 php orm doctrine model doctrine-orm

我想知道在Doctrine2中使用多对多关系的最好,最干净,最简单的方法是什么.

让我们假设我们有一张专辑,如Metallica的Master of Puppets,有几首曲目.但请注意,一首曲目可能会出现在一张专辑中,比如Metal 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

样本数据:

             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 |
+----+----------+----------+----------+------------+
Run Code Online (Sandbox Code Playgroud)

现在我可以显示与他们相关的专辑和曲目列表:

$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!' : ''
        );
    }   
}
Run Code Online (Sandbox Code Playgroud)

结果是我所期待的,即:一个专辑列表,其中的曲目以适当的顺序排列,而促销的专辑标记为已晋升.

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) 
Run Code Online (Sandbox Code Playgroud)

那有什么不对?

这段代码演示了什么错误:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

编辑

@beberlei建议使用代理方法:

class AlbumTrackReference {
    public function getTitle() {
        return $this->getTrack()->getTitle()
    }
}
Run Code Online (Sandbox Code Playgroud)

这将是一个好主意,但我使用的"参照物"双方:$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();
 }
Run Code Online (Sandbox Code Playgroud)

这不是一个非常干净的方式.

FMa*_*008 158

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

将多对多关系视为一个实体本身,然后你意识到你有三个对象,它们之间以一对多和多对一的关系相互关联.

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

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

  • "一旦关系有数据,它就不再是关系"这真的很有启发性.从实体角度来看,我无法想到关系! (6认同)

beb*_*lei 17

从$ album-> getTrackList()您将获得"AlbumTrackReference"实体,那么从Track和代理添加方法呢?

class AlbumTrackReference
{
    public function getTitle()
    {
        return $this->getTrack()->getTitle();
    }

    public function getDuration()
    {
        return $this->getTrack()->getDuration();
    }
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您的循环以及与循环专辑曲目相关的所有其他代码都会大大简化,因为所有方法都只是在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!' : ''
    );
}
Run Code Online (Sandbox Code Playgroud)

顺便说一下你应该重命名AlbumTrackReference(例如"AlbumTrack").它显然不仅是一个参考,还包含其他逻辑.由于可能还有轨道没有连接到专辑但只能通过促销CD或其他东西,这也允许更清晰的分离.

  • 你为什么不用两种方法?AlbumTrackReference对象上的getAlbumTitle()和getTrackTitle()?两者都代理各自的子对象. (3认同)

Wil*_*ilt 13

没有什么比这更好的例子了

对于寻找3个参与类之间的一对多/多对一关联的清晰编码示例以在关系中存储额外属性的人,请检查此站点:

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

想想你的主键

还要想想你的主键.您可以经常使用复合键来实现这样的关系.Doctrine原生支持这一点.您可以将引用的实体变为ID. 在此处查看复合键的文档


Ocr*_*ius 10

我想我会选择@beberlei使用代理方法的建议.你可以做些什么来简化这个过程是定义两个接口:

interface AlbumInterface {
    public function getAlbumTitle();
    public function getTracklist();
}

interface TrackInterface {
    public function getTrackTitle();
    public function getTrackDuration();
}
Run Code Online (Sandbox Code Playgroud)

然后,您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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,通过删除直接引用a Track或an的逻辑Album,并且只是替换它以便它使用TrackInterfaceor AlbumInterface,你就可以AlbumTrackReference在任何可能的情况下使用你的逻辑.您需要的是稍微区分接口之间的方法.

这不会区分DQL和Repository逻辑,但是你的服务只会忽略你传递一个Album或一个AlbumTrackReference或一个Track或一个的事实,AlbumTrackReference因为你隐藏了接口后面的一切:)

希望这可以帮助!


jsu*_*ggs 7

首先,我主要同意贝伯莱的建议.但是,您可能正在将自己设计成陷阱.您的域似乎正在考虑将标题作为曲目的自然键,这可能是您遇到的99%场景的情况.然而,如果电池的木偶大师是不同的版本(不同的长度,生活,声音,混音,翻唱等)比对版本的Metallica的集合.

根据你想要处理(或忽略)这种情况的方式,你可以去beberlei的建议路线,或者只是在Album :: getTracklist()中使用你建议的额外逻辑.就个人而言,我认为额外的逻辑是合理的,以保持您的API清洁,但两者都有其优点.

如果你确实希望适应我的用例,你可以让Tracks包含一个自我引用OneToMany到其他Tracks,可能是$ similarTracks.在这种情况下,轨道电池将有两个实体,一个用于The Metallica Collection,另一个用于Master of the Puppets.然后每个类似的Track实体将包含对彼此的引用.此外,这将摆脱当前的AlbumTrackReference类,并消除您当前的"问题".我同意它只是将复杂性转移到另一个点,但它能够处理以前无法实现的用例.


rom*_*anb 6

你要求"最好的方式",但没有最好的方法.有很多方法,你已经发现了其中一些.在使用关联类时,如何管理和/或封装关联管理完全取决于您和您的具体领域,没有人能够向您展示我害怕的"最佳方式".

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


Ons*_*hop 6

我从与关联类(具有其他自定义字段)注释中定义的连接表和在多对多注释中定义的连接表的冲突中获得.

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

解释和解决方案是由上面的FMaz008确定的.在我的情况下,这要归功于论坛" 教义注释问题 " 中的这篇文章.这篇文章提请注意关于ManyToMany单向关系的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;

}
Run Code Online (Sandbox Code Playgroud)