Doctrine2:参照テーブルに列を追加して多対多を処理する最良の方法


282

Doctrine2で多対多のリレーションを処理するための最良で最もクリーンで最も簡単な方法は何だろうと思います。

メタリカのMaster of Puppetsのようないくつかのトラックのアルバムがあるとします。ただし、MetallicaのBatteryのように、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();
 }

そして、それはあまりクリーンな方法ではありません。


2
AlbumTrackReferenceをどのように処理しますか?たとえば、$ album-> addTrack()または$ album-> removeTrack()?
Daniel

コンテキストについてのコメントを理解できませんでした。私の意見では、データはコンテキストに依存しません。程度$album->getTracklist()[12]である AlbumTrackRefオブジェクトなので、$album->getTracklist()[12]->getTitle()(プロキシ方式を使用している場合)は、常にトラックのタイトルを返します。一方で$track->getAlbums()[1]あるAlbumオブジェクトは、そう$track->getAlbums()[1]->getTitle()いつもアルバムのタイトルを返します。
ヴィニシウスFagundes

もう一つのアイデアは、上で使用しているAlbumTrackReference2つのプロキシのメソッド、getTrackTitle()およびgetAlbumTitle
ヴィニシウスFagundes

回答:


158

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

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

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

関係がデータを持つと、それは関係ではなくなります!


doctrineコマンドラインツールでこの新しいエンティティをymlスキーマファイルとして生成する方法を知っている人はいますか?このコマンド:app/console doctrine:mapping:import AppBundle yml元の2つのテーブルのmanyToManyリレーションを引き続き生成し、3番目のテーブルをエンティティと見なさずに無視します:/
Stphane

foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }@Crozinによって提供されるとの違いは何consider the relationship as an entityですか?彼が聞きたいのは、リレーショナルエンティティをスキップしてトラックのタイトルを取得する方法だと思いますforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
panda

6
「関係がデータを持つと、それは関係ではなくなります」これは本当に啓発的でした。エンティティの観点から関係を考えることができませんでした!
タマネギ

関係がすでに作成され、多対多として使用されている場合はどうでしょうか。多対多のフィールドを追加する必要があることに気づき、別のエンティティを作成しました。問題は、既存のデータと同じ名前の既存のテーブルがあると、友達になりたくないようだということです。誰かがこれを以前に試したことはありますか?
タイレリズム2017

不思議に思う人のために:テーブルが機能するときに(既に存在する)多対多のジョイントでエンティティを作成する場合、多対多を保持するエンティティは、代わりに新しいエンティティに1対多に適応する必要があります。また、外部へのインターフェース(以前の多対多のゲッター/セッター)は、おそらく変更する必要があります。
Jakumi

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")。これは明らかにリファレンスであるだけでなく、追加のロジックが含まれています。アルバムに接続されていないトラックもおそらくあるので、promo-cdか何かで利用できるだけなので、これにより、よりクリーンな分離も可能になります。


1
プロキシメソッドは問題を100%解決しません(編集を確認してください)。Btw You should rename the AlbumT(...)-良い点
Crozin

3
なぜ2つの方法がないのですか?AlbumTrackReferenceオブジェクトのgetAlbumTitle()およびgetTrackTitle()?どちらもそれぞれのサブオブジェクトにプロキシします。
beberlei 2010

目標は、最も自然なオブジェクトAPIです。$album->getTracklist()[1]->getTrackTitle()と同じくらい良い/悪い$album->getTracklist()[1]->getTrack()->getTitle()です。ただし、私は2つの異なるクラスを必要とするようです。1つはアルバム->トラック参照用、もう1つはトラック->アルバム参照用で、実装するのが難しいです。おそらく、これが今のところ最良の解決策です...
Crozin 2010

13

良い例に勝るものはありません

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

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

主キーについて考える

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


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

このように、a Trackまたはを直接参照しているロジックを削除し、Album単にTrackInterfaceorまたはを使用するように置き換えるだけで、あらゆるケースでAlbumInterfaceを使用できますAlbumTrackReference。必要なのは、インターフェース間のメソッドを少し区別することです。

これはDQLもリポジトリロジックを、区別しませんが、あなたのサービスはちょうどあなたが渡しているという事実を無視しますAlbumAlbumTrackReference、またはTrackまたはAlbumTrackReferenceインタフェースの背後に非表示にしたので、すべてを:)

お役に立てれば!


7

第一に、私は彼の提案についてほとんどベベルレイに同意します。しかし、あなたは自分を罠に仕掛けているかもしれません。ドメインは、タイトルがトラックの自然なキーであると考えているようです。これは、遭遇するシナリオの99%に当てはまる可能性があります。しかし、どのような場合はバッテリー操り人形のマスターは、上のバージョンとは異なるバージョン(異なる長さ、ライブ、アコースティック、リミックス、リマスター、など)であるメタリカコレクション

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

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


6

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

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


6

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

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

説明と解決策は、上記のFMaz008によって識別されたものです。私の状況では、フォーラム「Doctrine Annotation Question」のこの投稿のおかげでした。この投稿は、ManyToManyの単方向の関係に関するDoctrineのドキュメントに注目します。「関連付けエンティティクラス」を使用するアプローチに関する注意を見て、2つのメインエンティティクラス間の多対多アノテーションマッピングをメインエンティティクラスの1対多アノテーションと2つの「多対多」アノテーションに直接置き換えます。関連エンティティクラスの-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;

}

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;

3

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

解決策は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、追加のエンティティを作成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

それが役に立てば幸い


1
更新されたリンク:1-FAQ2-チュートリアル:複合主キー
m14t

それはまさに私が探していたものです、ありがとう!残念ながら、3番目のユースケースにはymlの例はありません。:(yml形式を使用して、3番目の使用例の例を共有できますか?私は本当に評価します:#
Stphane

私はあなたのケースの答えに追加しました;)
Mirza Selimovic

不正解です。エンティティは、ID(id)AUTOである必要はありません。それは間違っています、私は正しい例を作成しようとしています
Gatunox

正しくフォーマットされているかどうかを確認するため、新しい回答を投稿します
Gatunox、2015年

3

単方向。双方向にするために、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

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


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

これを徹底的に調べて、パフォーマンスの面で悪影響を受けないようにする必要があります。

現在の設定はシンプルで効率的であり、セマンティクスの一部が正しく理解されていなくても理解しやすくなっています。


0

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

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


1
これにより理論的には質問に答えることができますが、ここに答えの本質的な部分を含め、参照用のリンクを提供することが望ましいでしょう
Spontifixus

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();
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.