Zastanawiam się, jaki jest najlepszy, najczystszy i najprostszy sposób pracy z relacjami wiele do wielu w Doctrine2.
Załóżmy, że mamy album typu Master of Puppets Metalliki z kilkoma utworami. Należy jednak pamiętać, że jeden utwór może pojawiać się w więcej niż jednym albumie, podobnie jak Battery Metalliki - trzy albumy zawierają ten utwór.
Potrzebuję więc relacji wiele do wielu między albumami i ścieżkami, używając trzeciej tabeli z dodatkowymi kolumnami (np. Pozycji ścieżki w określonym albumie). Właściwie muszę użyć, jak sugeruje dokumentacja Doctrine, podwójnej relacji jeden do wielu, aby osiągnąć tę funkcjonalność.
/** @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;
}
}
Przykładowe dane:
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 |
+----+----------+----------+----------+------------+
Teraz mogę wyświetlić listę albumów i powiązanych z nimi utworów:
$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!' : ''
);
}
}
Wyniki są takie, jakich się spodziewam, tj .: lista albumów z ich utworami w odpowiedniej kolejności i promowanymi oznaczonymi jako promowane.
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)
Więc co jest nie tak?
Ten kod pokazuje, co jest nie tak:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
zwraca tablicę AlbumTrackReference
obiektów zamiast Track
obiektów. Nie mogę utworzyć metod proxy, co by było, gdyby jedno Album
i drugie Track
miałoby getTitle()
metodę? Mógłbym wykonać dodatkowe przetwarzanie w ramach Album::getTracklist()
metody, ale jaki jest najprostszy sposób, aby to zrobić? Czy jestem zmuszony napisać coś takiego?
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
EDYTOWAĆ
@beberlei zasugerował użycie metod proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
To byłby dobry pomysł, ale używam tego „obiektu referencyjnego” z obu stron: $album->getTracklist()[12]->getTitle()
i $track->getAlbums()[1]->getTitle()
dlatego getTitle()
metoda powinna zwracać różne dane w zależności od kontekstu wywołania.
Musiałbym zrobić coś takiego:
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();
}
I to nie jest bardzo czysty sposób.
$album->getTracklist()[12]
isAlbumTrackRef
object, więc$album->getTracklist()[12]->getTitle()
zawsze zwraca tytuł ścieżki (jeśli używasz metody proxy). Chociaż$track->getAlbums()[1]
jestAlbum
obiektem, więc$track->getAlbums()[1]->getTitle()
zawsze zwraca tytuł albumu.AlbumTrackReference
dwóch metod proxygetTrackTitle()
igetAlbumTitle
.Odpowiedzi:
Otworzyłem podobne pytanie na liście dyskusyjnej użytkowników Doctrine i otrzymałem naprawdę prostą odpowiedź;
rozważ relację wiele do wielu jako samą istotę, a następnie uświadomisz sobie, że masz 3 obiekty, połączone między nimi relacją jeden do wielu i wiele do jednego.
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Gdy relacja ma dane, nie jest już relacją!
źródło
app/console doctrine:mapping:import AppBundle yml
nadal generuj relację manyToMany dla dwóch oryginalnych tabel i po prostu zignoruj trzecią tabelę zamiast traktować ją jako całość:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
się @Crozin odconsider the relationship as an entity
? Myślę, że chciałby zapytać, jak pominąć relacyjny byt i odzyskać tytuł ścieżki, używającforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
Z $ album-> getTrackList () otrzymasz również encje „AlbumTrackReference”, więc co z dodawaniem metod ze ścieżki i proxy?
W ten sposób twoja pętla znacznie się upraszcza, podobnie jak wszystkie inne kody związane z zapętlaniem ścieżek albumu, ponieważ wszystkie metody są po prostu proxy wewnątrz AlbumTrakcReference:
Btw Powinieneś zmienić nazwę AlbumTrackReference (na przykład „AlbumTrack”). Jest to oczywiście nie tylko odniesienie, ale zawiera dodatkową logikę. Ponieważ prawdopodobnie istnieją również utwory, które nie są podłączone do albumu, ale są dostępne tylko na płycie CD lub w inny sposób, co pozwala na czystsze rozdzielenie.
źródło
Btw You should rename the AlbumT(...)
- dobry punkt$album->getTracklist()[1]->getTrackTitle()
jest tak dobry / zły jak$album->getTracklist()[1]->getTrack()->getTitle()
. Wydaje się jednak, że musiałbym mieć dwie różne klasy: jedną dla referencji albumu> i drugą dla referencji ścieżki> albumów - i to jest zbyt trudne do wdrożenia. To chyba najlepsze jak dotąd rozwiązanie ...Nic nie przebije ładnego przykładu
Dla osób szukających czystego kodowania przykładu powiązań jeden do wielu / wielu do jednego między 3 uczestniczącymi klasami w celu przechowywania dodatkowych atrybutów w relacji sprawdź tę witrynę:
dobry przykład skojarzeń jeden do wielu / wielu do jednego między 3 uczestniczącymi klasami
Pomyśl o swoich podstawowych kluczach
Pomyśl także o swoim kluczu podstawowym. Często możesz używać kluczy kompozytowych do takich relacji. Doctrine natywnie to popiera. Możesz przekształcić swoje odwołania w identyfikatory. Sprawdź dokumentację dotyczącą kluczy kompozytowych tutaj
źródło
Myślę, że wybrałbym sugestię @ beberlei dotyczącą korzystania z metod proxy. Aby uprościć ten proces, należy zdefiniować dwa interfejsy:
Następnie zarówno Ty, jak
Album
i TyTrack
możesz je zaimplementować, podczas gdyAlbumTrackReference
nadal mogą implementować oba, w następujący sposób:W ten sposób, poprzez usunięcie logiki, która jest bezpośrednio odnoszącego się
Track
alboAlbum
i po prostu zastąpienie go tak, że używaTrackInterface
lubAlbumInterface
, masz do wykorzystania swojejAlbumTrackReference
w każdym możliwym przypadku. Będziesz potrzebował nieco rozróżnić metody między interfejsami.To nie rozróżnia logiki DQL ani repozytorium, ale twoje usługi po prostu zignorują fakt, że przekazujesz an
Album
lub anAlbumTrackReference
, albo aTrack
lub an,AlbumTrackReference
ponieważ ukryłeś wszystko za interfejsem :)Mam nadzieję że to pomoże!
źródło
Po pierwsze, w większości zgadzam się z beberlei w sprawie jego sugestii. Możesz jednak projektować się w pułapkę. Wydaje się, że Twoja domena uważa ten tytuł za naturalny klucz do ścieżki, co prawdopodobnie ma miejsce w 99% scenariuszy, które napotkasz. Co jednak, jeśli Battery on Master of the Puppets to inna wersja (inna długość, live, akustyczna, remiks, remasterowana itp.) Niż wersja z kolekcji The Metallica .
W zależności od tego, jak chcesz obsłużyć (lub zignorować) ten przypadek, możesz albo wybrać sugerowaną trasę beberlei, albo po prostu skorzystać z proponowanej dodatkowej logiki w Album :: getTracklist (). Osobiście uważam, że dodatkowa logika jest uzasadniona dla utrzymania interfejsu API w czystości, ale oba mają swoje zalety.
Jeśli chcesz uwzględnić mój przypadek użycia, możesz mieć Ścieżki zawierające samodzielne odniesienia OneToMany do innych Ścieżek, być może $ podobnych Ścieżek. W tym przypadku byłyby dwa podmioty dla utworu Battery , jeden dla The Metallica Collection i jeden dla Master of the Puppets . Następnie każda podobna jednostka śledzenia zawierałaby odniesienie do siebie. Pozbyłoby się to także bieżącej klasy AlbumTrackReference i wyeliminowałoby obecny problem. Zgadzam się, że po prostu przenosi złożoność do innego punktu, ale jest w stanie obsłużyć przypadek użycia, którego wcześniej nie był w stanie.
źródło
Pytasz o „najlepszy sposób”, ale nie ma najlepszego sposobu. Istnieje wiele sposobów, a niektóre z nich już odkryłeś. To, jak chcesz zarządzać i / lub enkapsulować zarządzanie powiązaniami podczas korzystania z klas powiązań, zależy wyłącznie od Ciebie i Twojej konkretnej domeny, obawiam się, że nikt nie może pokazać Ci „najlepszego sposobu”.
Poza tym pytanie można znacznie uprościć, usuwając Doctrine i relacyjne bazy danych z równania. Istota twojego pytania sprowadza się do pytania o to, jak postępować z klasami asocjacyjnymi w zwykłym OOP.
źródło
Zacząłem od konfliktu z adnotacją zdefiniowaną w klasie asocjacji (z dodatkowymi polami niestandardowymi) i tabelą asocjacji zdefiniowaną w adnotacji „wiele do wielu”.
Wydaje się, że definicje odwzorowań w dwóch elementach z bezpośrednią relacją wiele do wielu powodują automatyczne tworzenie tabeli łączenia za pomocą adnotacji „joinTable”. Jednak tabela łączenia została już zdefiniowana przez adnotację w podstawowej klasie encji i chciałem, aby użyła ona własnych definicji pól tej klasy encji, aby rozszerzyć tabelę łączenia o dodatkowe pola niestandardowe.
Wyjaśnienie i rozwiązanie zostało podane powyżej przez FMaz008. W mojej sytuacji było to dzięki temu postowi na forum „ Doctrine Adnotation Question ”. Ten post zwraca uwagę na dokumentację Doctrine dotyczącą jednokierunkowych relacji ManyToMany . Spójrz na uwagę dotyczącą podejścia polegającego na zastosowaniu „klasy encji asocjacyjnej”, zastępując w ten sposób mapowanie adnotacji wiele do wielu bezpośrednio między dwiema klasami encji przez adnotację jeden do wielu w klasach encji głównych i dwie „wiele do -jedne ”adnotacje w klasie jednostek asocjacyjnych. W tym poście znajduje się przykład Modele asocjacyjne z dodatkowymi polami :
źródło
To naprawdę przydatny przykład. Brakuje w doktrynie dokumentacji 2.
Bardzo ci dziękuję.
Dla funkcji proxy można wykonać:
i
źródło
Chodzi o metadane, dane o danych. Miałem ten sam problem z projektem, nad którym obecnie pracuję, i musiałem poświęcić trochę czasu, próbując go rozgryźć. Jest to zbyt wiele informacji, aby opublikować tutaj, ale poniżej znajdują się dwa linki, które mogą być przydatne. Odnoszą się one do frameworka Symfony, ale są oparte na 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/
Powodzenia i miłych referencji Metalliki!
źródło
Rozwiązanie znajduje się w dokumentacji Doctrine. W FAQ możesz to zobaczyć:
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
A samouczek jest tutaj:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
Więc już nie róbcie,
manyToMany
ale musicie stworzyć dodatkowy byt i umieścićmanyToOne
w swoich dwóch bytach.DODAJ do komentarza @ f00bar:
to proste, musisz zrobić coś takiego:
Tworzysz więc encję ArticleTag
Mam nadzieję, że to pomoże
źródło
:(
Czy ktoś mógłby udostępnić przykład trzeciego przypadku użycia w formacie yml? Naprawdę przyjechałbym:#
Jednokierunkowy. Wystarczy dodać inversedBy: (obca nazwa kolumny), aby stała się dwukierunkowa.
Mam nadzieję, że to pomoże. Do zobaczenia.
źródło
Możesz osiągnąć to, co chcesz, dzięki dziedziczeniu tabeli klas, w której zmieniasz AlbumTrackReference na AlbumTrack:
I
getTrackList()
zawierałbyAlbumTrack
obiekty, których możesz użyć tak, jak chcesz:Musisz to dokładnie zbadać, aby upewnić się, że nie cierpisz pod względem wydajności.
Twoja obecna konfiguracja jest prosta, wydajna i łatwa do zrozumienia, nawet jeśli niektóre semantyki nie pasują do ciebie.
źródło
Podczas pobierania wszystkich ścieżek albumu z klasy albumu, wygenerujesz jeszcze jedno zapytanie o jeszcze jeden rekord. Wynika to z metody proxy. Jest inny przykład mojego kodu (patrz ostatni post w temacie): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Czy jest jakaś inna metoda rozwiązania tego? Czy jedno połączenie nie jest lepszym rozwiązaniem?
źródło
Oto rozwiązanie opisane w dokumentacji Doctrine2
źródło