Ich frage mich, was die beste, sauberste und einfachste Art ist, mit vielen-zu-vielen-Beziehungen in Doctrine2 zu arbeiten.
Nehmen wir an, wir haben ein Album wie Master of Puppets von Metallica mit mehreren Tracks. Bitte beachten Sie jedoch, dass ein Titel möglicherweise in mehr als einem Album enthalten ist, wie dies bei Battery by Metallica der Fall ist - drei Alben enthalten diesen Titel.
Was ich also brauche, ist eine Viele-zu-Viele-Beziehung zwischen Alben und Titeln, wobei eine dritte Tabelle mit einigen zusätzlichen Spalten verwendet wird (z. B. die Position des Titels im angegebenen Album). Tatsächlich muss ich, wie aus der Dokumentation von Doctrine hervorgeht, eine doppelte Eins-zu-Viele-Beziehung verwenden, 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 damit verbundenen Alben und Titel anzeigen:
$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, dh: eine Liste von Alben mit ihren Titeln in der richtigen Reihenfolge und beworbenen, die als beworben markiert sind.
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)
Also, was ist falsch?
Dieser Code zeigt, was falsch ist:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
Gibt ein Array von AlbumTrackReference
Objekten anstelle von Track
Objekten zurück. Ich kann keine Proxy-Methoden erstellen, was wäre, wenn beide, Album
und Track
hätte getTitle()
Methode? Ich könnte eine zusätzliche Verarbeitung innerhalb der Album::getTracklist()
Methode durchführen, aber wie geht das am einfachsten? Bin ich gezwungen, so etwas zu schreiben?
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
BEARBEITEN
@beberlei schlug vor, Proxy-Methoden zu verwenden:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Das wäre eine gute Idee, aber ich verwende dieses "Referenzobjekt" von beiden Seiten: $album->getTracklist()[12]->getTitle()
und $track->getAlbums()[1]->getTitle()
, daher getTitle()
sollte die Methode je nach Aufrufkontext unterschiedliche Daten zurückgeben.
Ich müsste so etwas tun wie:
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 kein sehr sauberer Weg.
$album->getTracklist()[12]
ist einAlbumTrackRef
Objekt, gibt also$album->getTracklist()[12]->getTitle()
immer den Titel des Tracks zurück (wenn Sie die Proxy-Methode verwenden). Während$track->getAlbums()[1]
es sich um einAlbum
Objekt handelt,$track->getAlbums()[1]->getTitle()
wird immer der Titel des Albums zurückgegeben.AlbumTrackReference
zwei Proxy-MethodengetTrackTitle()
undgetAlbumTitle
.Antworten:
Ich habe eine ähnliche Frage in der Doctrine-Benutzer-Mailingliste geöffnet und eine wirklich einfache Antwort erhalten.
Betrachten Sie die Viele-zu-Viele-Beziehung als eine Einheit selbst, und dann stellen Sie fest, dass Sie drei Objekte haben, die zwischen ihnen mit einer Eins-zu-Viele- und einer Viele-zu-Eins-Beziehung verbunden sind.
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Sobald eine Beziehung Daten enthält, ist sie keine Beziehung mehr!
quelle
app/console doctrine:mapping:import AppBundle yml
Generieren Sie immer noch manyToMany-Beziehungen für die ursprünglichen zwei Tabellen und ignorieren Sie einfach die dritte Tabelle, anstatt sie als Entität zu betrachten:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
@Crozin undconsider the relationship as an entity
? Ich denke, was er fragen möchte, ist, wie man die relationale Entität überspringt und den Titel eines Titelsforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
Von $ album-> getTrackList () erhalten Sie immer "AlbumTrackReference" -Entitäten zurück. Wie wäre es also mit dem Hinzufügen von Methoden aus dem Track und dem Proxy?
Auf diese Weise wird Ihre Schleife erheblich vereinfacht, ebenso wie alle anderen Codes, die sich auf das Schleifen der Titel eines Albums beziehen, da alle Methoden nur in AlbumTrakcReference als Proxy verwendet werden:
Übrigens sollten Sie die AlbumTrackReference umbenennen (zum Beispiel "AlbumTrack"). Es ist eindeutig nicht nur eine Referenz, sondern enthält zusätzliche Logik. Da es wahrscheinlich auch Tracks gibt, die nicht mit einem Album verbunden sind, sondern nur über eine Promo-CD oder ähnliches erhältlich sind, ermöglicht dies auch eine sauberere Trennung.
quelle
Btw You should rename the AlbumT(...)
- guter Punkt$album->getTracklist()[1]->getTrackTitle()
ist so gut / schlecht wie$album->getTracklist()[1]->getTrack()->getTitle()
. Es scheint jedoch, dass ich zwei verschiedene Klassen haben müsste: eine für Album-> Titelreferenzen und eine für Titel-> Albumreferenzen - und das ist zu schwer zu implementieren. Also wahrscheinlich ist das die beste Lösung bisher ...Nichts geht über ein schönes Beispiel
Wenn Sie nach einem sauberen Codierungsbeispiel für eine Eins-zu-Viele / Viele-zu-Eins-Zuordnung zwischen den drei teilnehmenden Klassen suchen, um zusätzliche Attribute in der Beziehung zu speichern, lesen Sie diese Site:
schönes Beispiel für Eins-zu-Viele / Viele-zu-Eins-Assoziationen zwischen den 3 teilnehmenden Klassen
Denken Sie an Ihre Primärschlüssel
Denken Sie auch an Ihren Primärschlüssel. Für solche Beziehungen können Sie häufig zusammengesetzte Schlüssel verwenden. Die Lehre unterstützt dies von Haus aus. Sie können Ihre referenzierten Entitäten in IDs umwandeln. Überprüfen Sie hier die Dokumentation zu zusammengesetzten Schlüsseln
quelle
Ich denke, ich würde dem Vorschlag von @ beberlei folgen, Proxy-Methoden zu verwenden. Um diesen Prozess zu vereinfachen, können Sie zwei Schnittstellen definieren:
Dann können sowohl Sie
Album
als auch SieTrack
sie implementieren, während dieAlbumTrackReference
beiden weiterhin wie folgt implementieren können:Auf diese Weise können Sie Ihre Logik, die direkt auf ein
Track
oder ein verweist, entfernenAlbum
und sie einfach so ersetzen, dass sie einTrackInterface
oder verwendetAlbumInterface
,AlbumTrackReference
in jedem Fall verwenden. Was Sie brauchen, ist, die Methoden zwischen den Schnittstellen ein wenig zu unterscheiden.Dies wird weder die DQL- noch die Repository-Logik unterscheiden, aber Ihre Dienste ignorieren einfach die Tatsache, dass Sie ein
Album
oder einAlbumTrackReference
oder einTrack
oder ein übergeben,AlbumTrackReference
weil Sie alles hinter einer Schnittstelle versteckt haben :)Hoffe das hilft!
quelle
Erstens stimme ich beberlei in seinen Vorschlägen größtenteils zu. Möglicherweise entwerfen Sie sich jedoch selbst in eine Falle. Ihre Domain scheint den Titel als natürlichen Schlüssel für einen Track zu betrachten, was wahrscheinlich bei 99% der Szenarien der Fall ist, auf die Sie stoßen. Was aber, wenn Batterie auf Meister der Puppen ist eine andere Version (unterschiedliche Länge, Live, akustisch, mischen sie wieder, remastered, usw.) als die Version auf der Metallica - Sammlung .
Je nachdem, wie Sie diesen Fall behandeln (oder ignorieren) möchten, können Sie entweder die von beberlei vorgeschlagene Route wählen oder einfach Ihre vorgeschlagene zusätzliche Logik in Album :: getTracklist () verwenden. Persönlich denke ich, dass die zusätzliche Logik gerechtfertigt ist, um Ihre API sauber zu halten, aber beide haben ihre Vorzüge.
Wenn Sie meinen Anwendungsfall berücksichtigen möchten, können Tracks ein selbstreferenzierendes OneToMany für andere Tracks enthalten, möglicherweise $ SimilarTracks. In diesem Fall gäbe es zwei Einheiten für die Track- Batterie , eine für die Metallica-Sammlung und eine für Master of the Puppets . Dann würde jede ähnliche Track-Entität einen Verweis aufeinander enthalten. Außerdem würde dies die aktuelle AlbumTrackReference-Klasse beseitigen und Ihr aktuelles "Problem" beseitigen. Ich stimme zu, dass es nur die Komplexität an einen anderen Punkt verschiebt, aber es ist in der Lage, einen Anwendungsfall zu handhaben, den es vorher nicht konnte.
quelle
Sie fragen nach dem "besten Weg", aber es gibt keinen besten Weg. Es gibt viele Möglichkeiten und einige davon haben Sie bereits entdeckt. Wie Sie das Assoziationsmanagement verwalten und / oder kapseln möchten, wenn Sie Assoziationsklassen verwenden, liegt ganz bei Ihnen und Ihrer konkreten Domäne. Ich fürchte, niemand kann Ihnen einen "besten Weg" zeigen.
Abgesehen davon könnte die Frage erheblich vereinfacht werden, indem Doktrin- und relationale Datenbanken aus der Gleichung entfernt werden. Das Wesentliche Ihrer Frage ist die Frage, wie Sie mit Assoziationsklassen in einfachem OOP umgehen sollen.
quelle
Ich hatte einen Konflikt mit der Join-Tabelle, die in einer Annotation einer Assoziationsklasse (mit zusätzlichen benutzerdefinierten Feldern) definiert ist, und einer Join-Tabelle, die in einer Annotation von vielen zu vielen definiert ist.
Die Zuordnungsdefinitionen in zwei Entitäten mit einer direkten Viele-zu-Viele-Beziehung schienen zur automatischen Erstellung der Verknüpfungstabelle unter Verwendung der Annotation 'joinTable' zu führen. Die Verknüpfungstabelle wurde jedoch bereits durch eine Anmerkung in der zugrunde liegenden Entitätsklasse definiert, und ich wollte, dass sie die eigenen Felddefinitionen dieser Zuordnungsentitätsklasse verwendet, um die Verknüpfungstabelle um zusätzliche benutzerdefinierte Felder zu erweitern.
Die Erklärung und Lösung ist die von FMaz008 oben angegebene. In meiner Situation war es diesem Beitrag im Forum ' Doctrine Annotation Question ' zu verdanken . Dieser Beitrag macht auf die Doctrine-Dokumentation zu ManyToMany Unidirektionalen Beziehungen aufmerksam . Lesen Sie den Hinweis zum Ansatz der Verwendung einer 'Assoziationsentitätsklasse', bei der die Zuordnung von vielen zu vielen Annotationen direkt zwischen zwei Hauptentitätsklassen durch eine Eins-zu-viele-Annotation in den Hauptentitätsklassen und zwei 'Viele-zu-Viele "ersetzt wird -one 'Annotationen in der assoziativen Entitätsklasse. In diesem Forumbeitrag finden Sie ein Beispiel für Assoziationsmodelle mit zusätzlichen Feldern :
quelle
Dieses wirklich nützliche Beispiel. Es fehlt in der Dokumentationslehre 2.
Vielen Dank.
Für die Proxys können folgende Funktionen ausgeführt werden:
und
quelle
Sie beziehen sich auf Metadaten, Daten über Daten. Ich hatte das gleiche Problem für das Projekt, an dem ich gerade arbeite, und musste einige Zeit damit verbringen, es herauszufinden. Es sind zu viele Informationen, um sie hier zu veröffentlichen, aber unten finden Sie zwei Links, die Sie möglicherweise nützlich finden. Sie verweisen auf das Symfony-Framework, basieren jedoch auf dem 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/
Viel Glück und schöne Metallica-Referenzen!
quelle
Die Lösung liegt in der Dokumentation von Doctrine. In den FAQ können Sie Folgendes sehen:
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
Und das Tutorial ist hier:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
Sie tun also nichts mehr,
manyToMany
sondern müssen eine zusätzliche Entität erstellen und diesemanyToOne
Ihren beiden Entitäten zuweisen.ADD für @ f00bar Kommentar:
Es ist einfach, Sie müssen nur so etwas tun:
Sie erstellen also eine Entität ArticleTag
Ich hoffe, es hilft
quelle
:(
Könnte jemand ein Beispiel des dritten Anwendungsfalls im yml-Format teilen? Ich würde wirklich appriace:#
Unidirektional. Fügen Sie einfach inversedBy: (Name der Fremdspalte) hinzu, um es bidirektional zu machen.
Ich hoffe, es hilft. Wir sehen uns.
quelle
Möglicherweise können Sie mit Class Table Inheritance das erreichen, was Sie wollen, indem Sie AlbumTrackReference in AlbumTrack ändern:
Und
getTrackList()
würdeAlbumTrack
Objekte enthalten , die Sie dann verwenden können, wie Sie möchten:Sie müssen dies gründlich prüfen, um sicherzustellen, dass Sie nicht unter der Leistung leiden.
Ihre aktuelle Einrichtung ist einfach, effizient und leicht zu verstehen, auch wenn einige der Semantiken nicht ganz richtig zu Ihnen passen.
quelle
Während alle Albumtitel innerhalb der Albumklasse erstellt werden, generieren Sie eine weitere Abfrage für einen weiteren Datensatz. Das liegt an der Proxy-Methode. Es gibt ein weiteres Beispiel für meinen Code (siehe letzten Beitrag im Thema): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Gibt es eine andere Methode, um das zu beheben? Ist ein einzelner Join nicht eine bessere Lösung?
quelle
Hier ist die Lösung, wie in der Doctrine2-Dokumentation beschrieben
quelle