291

Doctrine2で多対多の関係を処理するための最良の、最もクリーンで最も簡単な方法は何でしょうか。

メタリカのMasterofPuppetsのようないくつかのトラックのあるアルバムがあるとしましょう。ただし、 Battery by Metallicaのように、1つのトラックが複数のアルバムに表示される可能性があることに注意してください。3つのアルバムがこのトラックをフィーチャーしています。

したがって、必要なのは、いくつかの追加の列(指定されたアルバム内のトラックの位置など)を含む3番目のテーブルを使用した、アルバムとトラック間の多対多の関係です。実際、Doctrineのドキュメントが示唆しているように、その機能を実現するには、二重の1対多の関係を使用する必要があります。

/** @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();
 }

そして、それはあまりきれいな方法ではありません。

4

13 に答える 13

17

$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」など)。これは明らかに単なるリファレンスではなく、追加のロジックを含んでいます。アルバムには接続されていないが、プロモ CD などを通じてのみ利用できるトラックもおそらくあるので、これにより、より明確な分離も可能になります。

于 2010-08-25T11:53:20.590 に答える
10

プロキシメソッドを使用するという@beberleiの提案に同意すると思います。このプロセスを簡単にするためにできることは、次の 2 つのインターフェイスを定義することです。

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を使用するように置き換えるだけで、あらゆる場合に使用できます。必要なのは、インターフェイス間のメソッドを少し区別することです。TrackInterfaceAlbumInterfaceAlbumTrackReference

これは、DQL もリポジトリ ロジックも区別しませんが、インターフェースの背後にすべてを隠しているため、サービスは、 または 、またはAlbumまたはを渡しているという事実を無視します :)AlbumTrackReferenceTrackAlbumTrackReference

お役に立てれば!

于 2012-06-07T22:59:42.427 に答える
7

まず、beberlei の提案にはほぼ同意します。ただし、自分自身を罠にかけている可能性があります。あなたのドメインは、タイトルがトラックの自然なキーであると考えているようです。これは、遭遇するシナリオの 99% に当てはまる可能性があります。ただし、 Master of the Puppets のBatteryThe Metallica Collectionのバージョンとは異なるバージョン (長さ、ライブ、アコースティック、リミックス、リマスターなど) である場合はどうなるでしょうか。

そのケースをどのように処理するか (または無視するか) に応じて、beberlei の提案されたルートに進むか、Album::getTracklist() で提案された追加のロジックを使用することができます。個人的には、API をきれいに保つためにロジックを追加するのは正当だと思いますが、どちらにもメリットがあります。

私の使用例に対応したい場合は、トラックに、他のトラック (おそらく $similarTracks) への OneToMany を参照する自己参照を含めることができます。この場合、トラックBatteryには 2 つのエンティティがあり、1 つはThe Metallica Collection用で、もう 1 つはMaster of the Puppets用です。次に、同様の各 Track エンティティには、相互への参照が含まれます。また、現在の AlbumTrackReference クラスを取り除き、現在の「問題」を解消します。複雑さを別のポイントに移動しているだけであることに同意しますが、以前はできなかったユースケースを処理できます.

于 2010-09-29T21:50:07.643 に答える
6

関連付けクラス (追加のカスタム フィールドを含む) 注釈で定義された結合テーブルと、多対多注釈で定義された結合テーブルとの競合から取得していました。

直接の多対多の関係を持つ 2 つのエンティティのマッピング定義により、「joinTable」アノテーションを使用して結合テーブルが自動的に作成されるように見えました。ただし、結合テーブルは、基になるエンティティ クラスの注釈によって既に定義されており、追加のカスタム フィールドで結合テーブルを拡張するために、この関連付けエンティティ クラス独自のフィールド定義を使用する必要がありました。

説明と解決策は、上記の FMaz008 で特定されたものです。私の状況では、フォーラム ' Doctrine Annotation Question 'のこの投稿のおかげでした。この投稿は、ManyToMany 単方向関係に関する Doctrine ドキュメントに注意を向けます。「関連付けエンティティ クラス」を使用するアプローチに関する注記を参照してください。したがって、2 つのメイン エンティティ クラス間の多対多アノテーション マッピングを、メイン エンティティ クラスの 1 対多アノテーションと 2 つの「多対多アノテーション」に直接置き換えます。連想エンティティ クラスの -one' 注釈。このフォーラム投稿Association models with extra fields で提供されている例があります。

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;

}
于 2013-10-03T13:38:00.530 に答える
6

あなたは「最善の方法」を求めますが、最善の方法はありません。多くの方法があり、それらのいくつかはすでに発見されています。関連クラスを使用するときに関連管理をどのように管理および/またはカプセル化するかは、完全にあなたとあなたの具体的なドメイン次第です.誰もあなたに「最善の方法」を示すことはできません.

それとは別に、方程式からDoctrineとリレーショナルデータベースを削除することで、質問を大幅に簡素化できます. あなたの質問の本質は、プレーンな OOP で関連付けクラスを処理する方法についての質問に要約されます。

于 2010-08-25T18:59:41.073 に答える
3

これは本当に便利な例です。ドキュメンテーションの教義 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;
于 2010-10-29T00:26:45.620 に答える
2

AlbumTrackReference を AlbumTrack に変更するClass Table Inheritanceを使用して、目的を達成できる場合があります。

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!' : ''
    );
}

パフォーマンスが低下しないように、これを徹底的に調べる必要があります。

現在のセットアップはシンプルで効率的で、セマンティクスの一部が適切ではない場合でも理解しやすいものです。

于 2010-09-07T18:28:31.637 に答える
0

アルバム クラス内ですべてのアルバム トラック フォームを取得するときに、もう 1 つのレコードに対してもう 1 つのクエリを生成します。これはプロキシ方式によるものです。私のコードの別の例があります (トピックの最後の投稿を参照してください):

それを解決する他の方法はありますか?単一の結合はより良い解決策ではありませんか?

于 2011-11-03T21:15:37.173 に答える