Doctrine2: Der beste Weg, mit zusätzlichen Spalten in Referenztabelle many-to-many zu handhaben

StackOverflow https://stackoverflow.com/questions/3542243

Frage

Ich frage mich, was ist die beste, die sauberste und am einfachsten Weg zur Arbeit mit many-to-many-Beziehungen in Doctrine2.

Nehmen wir an, dass wir ein Album wie haben Master of Puppets von Metallica mit mehreren Spuren. Aber bitte beachten Sie, dass eine Spur Macht erscheint in mehr als ein Album, wie Batterie von Metallica tut -. drei Alben diesen Track sind mit

Also, was ich brauche, ist many-to-many-Beziehung zwischen Alben und Tracks, dritte Tabelle mit einigen zusätzlichen Spalten mit (wie Position der Spur in bestimmtem Album). Eigentlich muß ich verwenden, als Lehre in der Dokumentation schlug vor, eine doppelte Eins-zu-Viele-Beziehung, um diese Funktionalität zu erreichen.

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

Beispieldaten:

             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 |
+----+----------+----------+----------+------------+

Jetzt kann ich eine Liste der Alben und Tracks zugeordnet anzuzeigen:

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

Die Ergebnisse sind das, was ich erwarte, das heißt: eine Liste der Alben mit ihren Tracks in geeigneter Reihenfolge und gefördert diejenigen als gefördert markiert werden

.
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) 
So

was ist los?

Dieser Code zeigt, was falsch ist:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}

Album::getTracklist() gibt ein Array von AlbumTrackReference statt Track Objekte Objekte. Ich kann keine Proxy-Methoden erstellen Ursache was ist, wenn beide, Album und Track würde getTitle() Methode haben? Ich konnte einige zusätzliche Verarbeitung in Album::getTracklist() Methode tun, aber was ist der am einfachsten Weg, dies zu tun? Bin ich gezwungen, schreiben Sie so etwas?

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

EDIT

@beberlei vorgeschlagen Proxy-Methoden zu verwenden:

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

Das wäre eine gute Idee, aber ich bin mit, dass „Referenzobjekt“ von beiden Seiten. $album->getTracklist()[12]->getTitle() und $track->getAlbums()[1]->getTitle(), so getTitle() Verfahren unterschiedliche Daten basierend auf dem Kontext des Aufrufs zurückgeben sollten

Ich möchte etwas tun:

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

Und das ist nicht eine sehr saubere Art und Weise.

War es hilfreich?

Lösung

Ich habe eine ähnliche Frage in der Lehre Benutzer Mailing-Liste geöffnet und bekam eine wirklich einfache Antwort;

betrachten die viele zu viele Beziehung als Einheit selbst, und dann merkt man, Sie 3 Objekte haben, verbunden zwischen ihnen mit einer Eins-zu-viele und viele-zu-eins-Beziehung.

http://groups.google.com/ Gruppe / Lehre-user / browse_thread / Thema / d1d87c96052e76f7 / 436b896e83c10868 # 436b896e83c10868

Sobald eine Beziehung Daten hat, ist es nicht mehr eine Beziehung!

Andere Tipps

Von $ Album-> getTrackList () Sie alwas "AlbumTrackReference" get Einheiten zurück, so etwas über das Hinzufügen von Methoden aus dem Track and Proxy?

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

    public function getDuration()
    {
        return $this->getTrack()->getDuration();
    }
}

Auf diese Weise vereinfacht Ihre Schleife erheblich, sowie allen anderen Code im Zusammenhang mit Looping die Titel eines Albums, da alle Methoden innerhalb AlbumTrakcReference nur proxied sind:

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 Sie sollten die AlbumTrackReference (zum Beispiel "AlbumTrack") umbenennen. Es ist offensichtlich nicht nur eine Referenz, enthält aber zusätzliche Logik. Da gibt es wahrscheinlich auch Tracks, die nicht zu einem Album verbunden sind, aber nur zur Verfügung, durch eine Promo-CD oder etwas dies auch für eine saubere Trennung ermöglicht.

Nichts geht über ein schönes Beispiel

Für Menschen, für ein sauberes Codierung Beispiel für eine Eins-zu-viele / many-to-one Assoziationen zwischen den drei beteiligten Klassen suchen zusätzliche Attribute zu speichern, in der Beziehung diese Website finden Sie unter:

schönes Beispiel für eine eins-zu-viele / many-to-one Assoziationen zwischen den drei beteiligten Klassen

Denken Sie über Ihre Primärschlüssel

Denken Sie auch an Ihre Primärschlüssel. Sie können oft zusammengesetzte Schlüssel für Beziehungen wie folgt verwenden. Lehre nativ unterstützt dies. Sie können Ihre referenzierten Entitäten in ids machen. Überprüfen Sie die Dokumentation auf zusammengesetzte Schlüssel hier

ich glaube, ich mit @ beberlei Vorschlag der Verwendung von Proxy-Methoden gehen würde. Was Sie tun können diesen Prozess einfacher zu machen, ist zwei Schnittstellen zu definieren:

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

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

Dann werden sowohl Ihre Album und Ihre Track können sie implementieren, während die AlbumTrackReference beide noch umsetzen können, wie folgt:

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

Auf diese Weise, indem Sie Ihre Logik zu entfernen, die direkt ein Track oder eine Album verweisen, und ersetzt es einfach so, dass es ein TrackInterface oder AlbumInterface verwendet, erhalten Sie Ihre AlbumTrackReference in jedem Fall nicht zu verwenden. Was Sie brauchen, ist die Methoden zwischen den Schnittstellen ein wenig zu unterscheiden.

Dies wird nicht die DQL unterscheiden, noch die Repository-Logik, aber Ihre Dienste werden nur die Tatsache ignorieren, dass Sie einen Album oder eine AlbumTrackReference vorbei sind, oder eine Track oder eine AlbumTrackReference, weil Sie alles hinter einer Schnittstelle versteckt haben: )

Hope, das hilft!

Zunächst stimme ich meistens mit beberlei auf seine Vorschläge. Sie können aber selbst in eine Falle sein zu entwerfen. Ihre Domain erscheint der Titel werden unter Berücksichtigung der natürlichen Schlüssel für eine Spur zu sein, die für 99% der Szenarien Sie stoßen wahrscheinlich der Fall ist. Was aber, wenn Batterie auf Master of the Puppets ist eine andere Version (unterschiedliche Länge, Live, akustisch, mischen sie wieder, remastered, usw.) als die Version auf Die Metallica-Sammlung .

Je nachdem, wie Sie behandelt werden sollen (oder ignorieren) diesem Fall könnten Sie entweder beberlei Route vorgeschlagen hat, oder gehen Sie einfach mit Ihrem vorgeschlagene zusätzliche Logik in Album :: getTracklist (). Ich persönlich denke, die zusätzliche Logik gerechtfertigt ist Ihre API sauber zu halten, aber beide haben ihre Berechtigung.

Wenn Sie wünschen, tun meinen Anwendungsfall gerecht zu werden, könnten Sie haben Track enthalten eine selbst andere Tracks Referenzierung OneToMany, möglicherweise similarTracks $. In diesem Fall gäbe es zwei Einheiten für die Spur sein Batterie , ein für Die Metallica-Sammlung und ein für Master of the Puppets . Dann würde jede ähnliche Spur Einheit einen Verweis zueinander enthalten. Auch das wäre loszuwerden der aktuellen AlbumTrackReference Klasse und eliminieren Sie Ihre aktuelle „Ausgabe“. Ich stimme zu, dass es nur, um die Komplexität zu einem anderen Punkt zu bewegen, aber es ist in der Lage einen usecase zu handhaben es bisher nicht möglich war.

Sie fragen nach „besten Weg“, aber es gibt keine beste Art und Weise. Es gibt viele Möglichkeiten und Sie bereits einige von ihnen entdeckt. Wie Sie verwalten mögen und / oder encapsulate Verbandsmanagement beim Verein Klasse ist ganz Sie und Ihre konkrete Domäne, niemand kann Ihnen zeigen, einen „besten Weg,“ ich habe Angst.

Abgesehen davon könnte die Frage durch Entfernen Lehre und relationale Datenbanken aus der Gleichung viel vereinfacht werden. Das Wesen der Frage läuft darauf hinaus, auf eine Frage, wie man mit Assoziationsklassen im Klar OOP zu behandeln.

Ich wurde von einem Konflikt immer mit Join-Tabelle in einer Assoziationsklasse definiert (mit zusätzlichen benutzerdefinierten Feldern) Annotation und eine Join-Tabelle in einer many-to-many-Annotation definiert.

Die Mapping-Definitionen in zwei Einheiten mit einer direkten many-to-many-Beziehung erschienen in der automatischen Erstellung der Join-Tabelle mit der ‚joinTable‘ Anmerkung zu führen. Allerdings kommt die Tabelle bereits mit einer Anmerkung in den zugrunde liegenden Entitätsklasse definiert wurde und ich wollte es diese Vereinigung Entitätsklasse eigene Felddefinitionen verwenden, um die Join-Tabelle mit zusätzlichen benutzerdefinierten Feldern zu erweitern.

Die Erklärung und Lösung ist, dass oberhalb von FMaz008 identifiziert. In meiner Situation war es dank diesen Beitrag im Forum ‚ Lehre Annotation Frage ‘. Dieser Beitrag verweist auf die Lehre Dokumentation in Bezug auf ManyToMany Uni-direktionale Beziehungen . Schauen Sie sich die Notiz über den Ansatz der Verwendung eine Vereinigung Entitätsklasse 'so die many-to-many-Annotation-Mapping direkt zwischen zwei Häuptern Entitätsklassen mit einer in den wichtigsten Entitätsklassen und zwei ‚one-to-many-Anmerkung ersetzt many-to -on‘Anmerkungen in der assoziativen Entitätsklasse. Es gibt ein Beispiel in diesem Forum posten bereitgestellt Verband Modelle mit zusätzlichen Feldern :

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;

}

Das ist wirklich nützliches Beispiel. Es fehlt in der Dokumentation Lehre 2.

Sehr danke.

Für die Proxies können Funktionen durchgeführt werden:

class AlbumTrack extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {} 
}

class TrackAlbum extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {}
}

class AlbumTrackAbstract {
   private $id;
   ....
}

und

/** @OneToMany(targetEntity="TrackAlbum", mappedBy="album") */
protected $tracklist;

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;

Was Sie sich beziehen ist Metadaten, Daten über Daten. Ich hatte das gleiche Problem für das Projekt, das ich aktuell arbeite und musste einige Zeit damit verbringen, um es herauszufinden. Es ist zu viele Informationen hier zu veröffentlichen, aber unten sind zwei Links, die Sie möglicherweise nützlich finden. Sie tun das Symfony Framework verweisen, sind aber auf der Grundlage der Lehre ORM.

http: // melikedev .com / 2010/04/06 / symfony Spar-Metadaten-in-form-save-Art-ids /

http: / /melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/

Viel Glück und schöne Metallica Referenzen!

Die Lösung ist in der Dokumentation der Lehre. In der FAQ können Sie sehen so aus:

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

Und das Tutorial ist hier:

http: //docs.doctrine-project. org / en / 2.1 / tutorials / Composite-Primär keys.html

Sie brauchen also nicht mehr eine manyToMany tun, aber Sie haben eine zusätzliche Entity und Put-manyToOne auf Ihre beiden Entitäten erstellen.

Hinzufügen für @ f00bar Kommentar:

Es ist ganz einfach, man muss nur etwas tun:

Article  1--N  ArticleTag  N--1  Tag

So erstellen Sie eine Einheit 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

Ich hoffe, es hilft

unidirektional. Fügen Sie einfach die inversedBy. (Foreign Spaltenname) es Bidirektionale zu machen

# 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

Ich hoffe, es hilft. Wir sehen uns.

Unter Umständen können Sie erreichen, was Sie wollen mit Class Table Inheritance wo Sie AlbumTrackReference zu AlbumTrack ändern:

class AlbumTrack extends Track { /* ... */ }

Und getTrackList() würde AlbumTrack Objekte enthalten, die Sie dann wie Sie nutzen könnten wollen:

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

Sie müssen diese gründlich zu untersuchen, um Sie nicht sicher, leiden Performance-weise.

Ihr aktuelle Set-up ist einfach, effizient und einfach, auch zu verstehen, wenn einige der Semantik nicht ganz sitzen direkt mit Ihnen.

Während immer alle Album-Tracks innerhalb Album Klasse bilden, werden Sie eine weitere Abfrage für einen weiteren Datensatz erzeugen. Das ist, weil die Proxy-Methode. Es ist ein weiteres Beispiel für meinen Code (letzten Beitrag zum Thema sehen): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Gibt es eine andere Methode, das zu lösen? Ist das nicht eine einzige verbinden, um eine bessere Lösung?

Dies ist die Lösung, wie in der Doctrine2 Dokumentation

<?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();
    }
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top