Doctrine2: najlepszy sposób obsługi wielu do wielu z dodatkowymi kolumnami w tabeli odniesienia

Zastanawiam się, jaki jest najlepszy, najczystszy i najprostszy sposób pracy z wieloma relacjami w Doktrynie2.

Załóżmy, że mamy album jak Master of Puppets by Metallica z kilkoma utworami. Ale proszę zwrócić uwagę na fakt, że jeden utwór może pojawić się w więcej niż jednym albumie, jak Battery by Metallica does-trzy albumy zawierają ten utwór.

Więc potrzebuję wielu relacji między albumy i utwory, używając trzeciej tabeli z dodatkowymi kolumnami (np. pozycja utworu w określonym albumie). Właściwie muszę użyć, jak sugeruje dokumentacja doktryny, 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 utworów z nimi związanych:

$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ą tym, czego się spodziewam, czyli: listą albumów z ich utworami w odpowiedniej kolejności, a promowanymi są oznaczony jako promowany.

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) 
Co się stało?

Ten kod pokazuje co jest nie tak:

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

Album::getTracklist() zwraca tablicę obiektów AlbumTrackReference zamiast obiektów Track. Nie mogę utworzyć metod proxy, bo co jeśli obie, Album i Track miałyby metodę getTitle()? Mógłbym zrobić dodatkowe przetwarzanie w metodzie Album::getTracklist(), ale jaki jest najprostszy sposób na to? 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

EDIT

@Beberlei zasugerował użycie proxy metody:
class AlbumTrackReference {
    public function getTitle() {
        return $this->getTrack()->getTitle()
    }
}

To byłby dobry pomysł, ale używam tego "obiektu odniesienia" z obu stron: $album->getTracklist()[12]->getTitle() i $track->getAlbums()[1]->getTitle(), więc getTitle() metoda powinna zwracać różne dane na podstawie 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 zbyt czysty sposób.

Author: S.L. Barth, 2010-08-22

14 answers

Otworzyłem podobne pytanie na liście dyskusyjnej Doctrine user i dostałem naprawdę prostą odpowiedź;

Rozważ relację wiele do wielu jako byt sam w sobie, a potem zdajesz sobie sprawę, ż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ą !

 152
Author: FMaz008,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-02 14:10:41

From $album->getTrackList() you will alwas get" AlbumTrackReference " entities back, so what about adding methods from the Track and proxy?

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

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

W ten sposób Twoja pętla znacznie upraszcza, podobnie jak wszystkie inne kody związane z zapętlaniem ścieżek albumu, ponieważ wszystkie metody są po prostu proxy wewnątrz 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 powinieneś zmienić nazwę albumu (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ą połączone z albumem, ale po prostu dostępne przez promo-cd lub coś, co pozwala na czystsze oddzielenie również.

 17
Author: beberlei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-25 17:00:17

Nic nie przebije ładnego przykładu

Dla osób szukających czystego kodowania przykład powiązań jeden do wielu/wiele do jednego między 3 uczestniczącymi klasami, aby zapisać dodatkowe atrybuty w relacji sprawdź tę stronę:

Ładny przykład powiązań jeden do wielu / wiele do jednego między 3 uczestniczącymi klasami

Think about your primary keys

Pomyśl także o swoim głównym kluczu. Często można używać klawiszy kompozytowych do takie związki. Doktryna natywnie to popiera. Możesz przekształcić powiązane podmioty w identyfikatory. sprawdź dokumentację dotyczącą kluczy złożonych tutaj

 13
Author: Wilt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-22 07:55:51

Myślę, że zgodziłbym się z sugestią @beberlei dotyczącą używania metod proxy. Co można zrobić, aby ten proces uprościć jest zdefiniowanie dwóch interfejsów:

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

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

Wtedy zarówno Twoje Album, jak i twoje Track mogą je zaimplementować, podczas gdy AlbumTrackReference nadal mogą zaimplementować oba, jak następuje:

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

W ten sposób, usuwając swoją logikę, która bezpośrednio odnosi się do Track lub Album, i po prostu zastępując ją tak, aby używała TrackInterface lub AlbumInterface, możesz użyć swojej AlbumTrackReference w każdym możliwym przypadku. Co? trzeba będzie nieco odróżnić metody między interfejsami.

To nie odróżni DQL ani logiki repozytorium, ale twoje usługi zignorują fakt, że mijasz Album lub AlbumTrackReference, lub Track lub AlbumTrackReference, ponieważ Ukryłeś wszystko za interfejsem:)

Mam nadzieję, że to pomoże!

 9
Author: Ocramius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-07 23:23:49

Po pierwsze, w większości zgadzam się z beberlei co do jego sugestii. Jednak możesz być projektując się w pułapkę. Wydaje się, że Twoja domena uważa tytuł za naturalny klucz dla utworu, co prawdopodobnie ma miejsce w przypadku 99% scenariuszy, z którymi się spotykasz. Co jednak, jeśli Battery na Master of the Puppets jest inną wersją (różna długość, live, acoustic, remix, remastered, itp.) niż wersja na The Metallica Collection.

W zależności w jaki sposób chcesz obsłużyć (lub zignorować) tę sprawę, możesz albo przejść do sugerowanej trasy beberlei, albo po prostu przejść z proponowaną dodatkową logiką W metoda Album:: getTracklist (). Osobiście uważam, że dodatkowa logika jest uzasadniona, aby utrzymać API w czystości, ale oba mają swoje zalety.

Jeśli chcesz dostosować mój przypadek użycia, możesz mieć utwory zawierające samo odniesienie OneToMany do innych utworów, ewentualnie $ similarTracks. W tym przypadku byłyby dwa byty dla toru Baterii , jeden dla The Metallica Collection i jeden dla Master of the Puppets. Wtedy każda podobna Jednostka ścieżki zawierałaby odniesienie do siebie. Ponadto, to by pozbyć się bieżącej klasy AlbumTrackReference i wyeliminować obecny "problem". Zgadzam się, że po prostu przenosi złożoność do innego punktu, ale jest w stanie obsłużyć bazę użytkową, której wcześniej nie był w stanie.

 7
Author: jsuggs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-29 21:50:07

Prosisz o "najlepszy sposób", ale nie ma najlepszego sposobu. Istnieje wiele sposobów, a niektóre z nich już odkryłeś. Jak chcesz zarządzać i / lub enkapsulować zarządzanie asocjacjami podczas korzystania z klas asocjacyjnych zależy wyłącznie od Ciebie i Twojej konkretnej domeny, nikt nie może pokazać ci "najlepszego sposobu", obawiam się.

Poza tym pytanie można by znacznie uprościć, usuwając z równania doktryny i relacyjne bazy danych. Istota pytania sprowadza się do pytania o tym, jak radzić sobie z klasami asocjacyjnymi w plain OOP.

 6
Author: romanb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-25 18:59:41

Miałem konflikt z tabelą join zdefiniowaną w klasie asocjacji ( z dodatkowymi polami niestandardowymi ) adnotacją i tabelą join zdefiniowaną w adnotacji wielu do wielu.

Definicje mapowania w dwóch obiektach z bezpośrednią relacją wiele do wielu wydają się skutkować automatycznym tworzeniem tabeli łączenia za pomocą adnotacji 'joinTable'. Jednak tabela join została już zdefiniowana przez adnotację w swojej podstawowej klasie encji i chciałem, aby użyła tego Asocjacja własnych definicji pól klasy entity w celu rozszerzenia tabeli join o dodatkowe pola niestandardowe.

Wyjaśnienie i rozwiązanie jest zidentyfikowane przez fmaz008 powyżej. W mojej sytuacji to właśnie dzięki temu postowi na forum "pytanie o adnotację do doktryny ". Ten post zwraca uwagę na dokumentację doktrynalną dotyczącą wielu związków jednokierunkowych . Spójrz na notatkę dotyczącą podejścia polegającego na używaniu "klasy jednostek asocjacyjnych", zastępując w ten sposób wiele do wielu adnotacji mapowanie bezpośrednio między dwoma głównymi klasami encji z adnotacją jeden do wielu w głównych klasach encji i dwie adnotacje "wiele do jednego" w klasie encji asocjacyjnej. Jest przykład podany w tym poście na forum Modele asocjacyjne z dodatkowymi polami :

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;

}
 5
Author: Ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-03 13:57:15

To naprawdę przydatny przykład. Brakuje w doktrynie dokumentacji 2.

Bardzo dziękuję.

Dla funkcji proxy można wykonać:

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

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

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

I

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

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;
 3
Author: Anthony,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-29 00:26:45

To, o czym mówisz, to metadane, dane o danych. Miałem ten sam problem w projekcie, nad którym obecnie pracuję i musiałem poświęcić trochę czasu, próbując go rozgryźć. To zbyt wiele informacji, aby pisać tutaj, ale poniżej znajdują się dwa linki, które mogą okazać się przydatne. Odwołują się do frameworka Symfony, ale opierają się na doktrynie 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 odniesień do Metalliki!

 3
Author: Mike Purcell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-03 21:20:23

Rozwiązanie jest w dokumentacji doktryny. W FAQ możesz zobaczyć to:

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

A tutorial jest tutaj:

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

Więc nie robisz już manyToMany, ale musisz utworzyć dodatkowy byt i umieścić manyToOne na swoich dwóch bytach.

Dodaj dla @f00bar komentarz:

To proste, trzeba po prostu zrobić coś takiego:

Article  1--N  ArticleTag  N--1  Tag

Więc tworzysz artykuł encji

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

Mam nadzieję, że to pomoże

 3
Author: Mirza Selimovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-23 13:53:27

Jednokierunkowa. Po prostu dodaj inversedBy: (obca Nazwa kolumny), aby była dwukierunkowa.

# 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
Mam nadzieję, że to pomoże. Do zobaczenia.
 3
Author: Gatunox,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-27 03:40:58

Możesz być w stanie osiągnąć to, co chcesz z dziedziczenie tabeli klas gdzie zmienisz AlbumTrackReference na AlbumTrack:

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

I getTrackList() zawierałyby AlbumTrack obiekty, które następnie można by używać tak, jak chcesz:

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

Będziesz musiał to dokładnie zbadać, aby upewnić się, że nie cierpisz z powodu wydajności.

Twoja obecna konfiguracja jest prosta, wydajna i łatwa do zrozumienia, nawet jeśli niektóre semantyki nie pasują do ciebie.

 2
Author: rojoca,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-07 18:28:31

Podczas pobierania wszystkich utworów z albumu w klasie album, wygenerujesz jeszcze jedno zapytanie o jeszcze jeden rekord. To z powodu metody proxy. Jest jeszcze jeden 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, aby to rozwiązać? Czy jedno połączenie nie jest lepszym rozwiązaniem?

 0
Author: quba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-03 22:08:16

Oto rozwiązanie opisane w dokumentacji 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
Author: medunes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-31 13:29:57