web-dev-qa-db-ja.com

Doctrine2:参照テーブルに余分な列がある多対多を処理する最良の方法

私はDoctrine2で多対多のリレーションを操作するための最良の方法、最もクリーンな方法、そして最も簡単な方法を知りたいと思っています。

Master of Puppetsby Metallica のようなアルバムに複数のトラックがあると仮定しましょう。しかし、 Batteryby Metallica のように、1つのトラックが複数のアルバムに表示される可能性があることに注意してください-3つのアルバムはこのトラックをフィーチャー。

必要なのは、追加の列(指定されたアルバム内のトラックの位置など)を含む3番目のテーブルを使用して、アルバムとトラックの多対多の関係です。実際、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();
 }

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

274
Crozin

Doctrineユーザーメーリングリストで同様の質問を開いたところ、本当に簡単な答えが得られました。

多対多のリレーションをエンティティ自体と見なすと、3つのオブジェクトが1対多および多対1のリレーションでリンクされていることがわかります。

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

リレーションがデータを取得すると、リレーションではなくなります!

156
FMaz008

$ album-> getTrackList()から「AlbumTrackReference」エンティティを取得します。Trackとプロキシからメソッドを追加するのはどうでしょうか?

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

Btw AlbumTrackReferenceの名前を変更する必要があります(「AlbumTrack」など)。これは明らかに参照であるだけでなく、追加のロジックが含まれています。アルバムには接続されていないが、プロモーションCDなどで利用できるトラックもおそらく存在するため、これにより、よりクリーンな分離が可能になります。

17
beberlei

ニースの例に勝るものはありません

リレーションに追加の属性を保存するために、3つの参加クラス間の1対多/多対1の関連付けのクリーンなコーディング例を探している人は、このサイトをチェックしてください。

つの参加クラス間の1対多/多対1の関連付けの良い例

主キーについて考える

主キーについても考えてください。多くの場合、このような関係には複合キーを使用できます。 Doctrineはこれをネイティブにサポートします。参照されたエンティティをIDにできます。 ここで複合キーのドキュメントを確認してください

13
Wilt

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

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

interface TrackInterface {
    public function getTrackTitle();
    public function getTrackDuration();
}

次に、AlbumTrackの両方でそれらを実装できますが、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を渡しているという事実を単に無視します。これは、インターフェイスの背後にすべてを隠したからです:)

お役に立てれば!

10
Ocramius

最初に、私は彼の提案についてbeberleiにほぼ同意します。ただし、自分自身をトラップに設計している可能性があります。ドメインは、タイトルがトラックの自然なキーであると考えているようです。これは、遭遇するシナリオの99%に当てはまる可能性があります。ただし、Battery on Master of the Puppetsのバージョンがメタリカコレクション

その場合の処理​​方法(または無視)に応じて、beberleiの推奨ルートを使用するか、Album :: getTracklist()で提案された追加ロジックを使用するだけです。個人的には、APIをクリーンに保つために追加のロジックが正当化されると思いますが、どちらにもメリットがあります。

私のユースケースに対応したい場合は、OneToManyを他のトラック(おそらく$ similarTracks)に参照する自己をトラックに含めることができます。この場合、トラックには2つのエンティティバッテリー、1つはメタリカコレクション、1つはマスターオブザパペットになります。次に、類似の各Trackエンティティには、相互参照が含まれます。また、それは現在のAlbumTrackReferenceクラスを取り除き、現在の「問題」を排除します。複雑さを別のポイントに移動しているだけであることに同意しますが、以前は不可能だったユースケースを処理できます。

7
jsuggs

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

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

6
romanb

関連クラス(追加のカスタムフィールド)アノテーションで定義された結合テーブルと多対多アノテーションで定義された結合テーブルとの競合から取得していました。

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

説明と解決策は、上記のFMaz008で特定されたものです。私の状況では、フォーラム ' Doctrine Annotation Question 'のこの投稿に感謝しました。この投稿では、 ManyToMany単方向関係 に関するDoctrineのドキュメントに注目しています。 「関連付けエンティティクラス」を使用して、2つのメインエンティティクラス間の多対多のアノテーションマッピングを、メインエンティティクラスの1対多のアノテーションと2つの「多対多のアノテーション」に直接置き換える方法に関する注意をご覧ください。連想エンティティクラスの1つの注釈。このフォーラムの投稿で提供されている例があります 追加フィールドを持つ関連付けモデル

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;

}
6
Onshop

解決策はDoctrineのドキュメントにあります。 FAQでこれを見ることができます:

http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table

そして、チュートリアルはこちらです:

http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html

したがって、もうmanyToManyを行うことはありませんが、追加のEntityを作成し、manyToOneを2つのエンティティに配置する必要があります。

@ 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

私はそれが役立つことを願っています

3
Mirza Selimovic

これは本当に便利な例です。ドキュメントdoctrineに欠けています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;
3
Anthony

単方向。 inversedBy:(Foreign Column Name)を追加して、双方向にします。

# 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

役に立てば幸いです。またね。

3
Gatunox

あなたが言及しているのは、メタデータ、データに関するデータです。私が現在取り組んでいるプロジェクトにも同じ問題があり、それを理解しようとして少し時間を費やさなければなりませんでした。ここに投稿するには情報が多すぎますが、以下に役立つ2つのリンクを示します。 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/

幸運を祈ります、そしてニースメタリカの参考文献!

3
Mike Purcell

Class Table Inheritance で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!' : ''
    );
}

これを徹底的に検証して、パフォーマンスの低下を防ぐ必要があります。

現在の設定はシンプルで効率的であり、セマンティクスの一部があなたにぴったりではない場合でも理解しやすいです。

2
rojoca

アルバムクラス内のすべてのアルバムトラックフォームを取得しながら、もう1つのレコードに対してもう1つのクエリを生成します。これは、プロキシメソッドのためです。私のコードの別の例があります(トピックの最後の投稿を参照): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

それを解決する他の方法はありますか?単一の結合はより良いソリューションではありませんか?

0
quba

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
medunes