Doctrine2: la meilleure façon de gérer plusieurs à plusieurs avec des colonnes supplémentaires dans le tableau de référence

je me demande Quelle est la meilleure, la plus propre et la plus simple façon de travailler avec les relations de plusieurs à plusieurs en Doctrine2.

supposons que nous ayons un album comme Master of Puppets de Metallica avec plusieurs titres. Mais s'il vous plaît noter le fait qu'une piste pourrait apparaître dans plus d'un album, comme batterie par Metallica does - trois albums sont mettant en vedette cette piste.

donc ce dont j'ai besoin c'est d'une relation de plusieurs à plusieurs entre les albums et les pistes, en utilisant le troisième tableau avec quelques colonnes supplémentaires (comme la position de la piste dans l'album spécifié). En fait, je dois utiliser, comme la documentation de la Doctrine le suggère, une double relation un-à-plusieurs pour atteindre cette fonctionnalité.

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

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

les données de l'Échantillon:

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

maintenant je peux afficher une liste des albums et des pistes associés pour eux:

$dql = '
    SELECT   a, tl, t
    FROM     EntityAlbum 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) %sn", 
            $track->getPosition(),
            $track->getTrack()->getTitle(),
            $track->getTrack()->getDuration()->format('H:i:s'),
            $track->isPromoted() ? ' - PROMOTED!' : ''
        );
    }   
}

les résultats sont ce que j'attends, c'est à dire: une liste d'albums avec leurs titres dans l'ordre approprié et promus ceux étant marqués comme promus.

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) 

alors qu'est-ce qui ne va pas?

ce code montre ce qui ne va pas:

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

Album::getTracklist() retourne un tableau d'objets AlbumTrackReference au lieu des objets Track . Je ne peux pas créer de méthodes par procuration. tous les deux, Album et Track aurait la méthode getTitle() ? Je pourrais faire un traitement supplémentaire dans le cadre de la méthode Album::getTracklist() , mais quelle est la façon la plus simple de le faire? Suis-je obligé de faire écrire quelque chose comme ça?

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 suggère d'utiliser des méthodes de procuration:

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

ce serait une bonne idée mais j'utilise cet "objet de référence" des deux côtés: $album->getTracklist()[12]->getTitle() et $track->getAlbums()[1]->getTitle() , ainsi getTitle() méthode devrait retourner des données différentes basées sur le contexte de l'invocation.

je dois faire quelque chose comme:

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

Et ce n'est pas très propre.

266
demandé sur S.L. Barth 2010-08-22 20:16:10

14 réponses

j'ai ouvert une question similaire dans la liste de diffusion Doctrine user et j'ai eu une réponse vraiment simple;

considérez la relation de plusieurs à plusieurs comme une entité elle-même, et alors vous réalisez que vous avez 3 objets, liés entre eux par une relation de un à plusieurs et de plusieurs à un.

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

Une fois qu'une relation a des données, ce n'est plus une relation !

152
répondu FMaz008 2011-11-02 14:10:41

à partir de $ album - > getTrackList () vous récupérerez toujours les entités "AlbumTrackReference", alors qu'en est-il de l'ajout de méthodes à partir de la piste et du proxy?

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

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

de cette façon votre boucle simplifie considérablement, ainsi que tout autre code lié à la boucle des pistes d'un album, puisque toutes les méthodes sont tout simplement proxied à L'intérieur Albumtrakcref:

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 vous devez renommer L'Albumtrackref (par exemple"AlbumTrack"). Il est clairement, non seulement une référence, mais contient de la logique supplémentaire. Puisqu'il y a probablement aussi des morceaux qui ne sont pas connectés à un album mais simplement disponibles via un promo-cd ou quelque chose qui permet une séparation plus nette également.

17
répondu beberlei 2010-08-25 17:00:17

Rien ne vaut un bon exemple

Pour les personnes à la recherche d'un codage propre exemple de l'un-à-plusieurs ou plusieurs-à-un les associations entre les 3 classes participantes pour stocker des attributs supplémentaires dans la relation à consulter ce site:

bel exemple d'association un-à-plusieurs/plusieurs-à-un entre les 3 classes participantes

Pensez à vos clés primaires

pense aussi à ta clé primaire. Vous pouvez souvent utiliser des clés composites pour des relations comme celle-ci. La Doctrine soutient nativement ceci. Vous pouvez transformer vos entités référencées en identifiants. Vérifiez la documentation sur les clés composites ici

13
répondu Wilt 2017-11-22 07:55:51

je pense que je suivrais la suggestion de @beberlei d'utiliser des méthodes par procuration. Ce que vous pouvez faire pour simplifier ce processus est de définir deux interfaces:

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

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

alors, tant votre Album que votre Track peuvent les mettre en œuvre, tandis que le AlbumTrackReference peut encore mettre en œuvre les deux, comme suit:

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

de cette façon, en supprimant votre logique qui se réfère directement à un Track ou un Album , et juste en le remplaçant pour qu'il utilise un TrackInterface ou AlbumInterface , vous pouvez utiliser votre AlbumTrackReference dans tous les cas possibles. Ce que vous aurez besoin de différencier les méthodes entre les interfaces un peu.

cela ne différenciera pas le DQL ni la logique du dépôt, mais vos services ignoreront simplement le fait que vous passez un Album ou un AlbumTrackReference , ou un Track ou un AlbumTrackReference parce que vous avez tout caché derrière une interface :)

Espérons que cette aide!

9
répondu Ocramius 2012-06-07 23:23:49

tout d'abord, je suis plutôt d'accord avec beberlei sur ses suggestions. Cependant, vous pouvez vous concevoir dans un piège. Votre domaine semble considérer le titre comme la clé naturelle d'une piste, ce qui est probablement le cas pour 99% des scénarios que vous rencontrez. Cependant, que faire si batterie sur Maître des marionnettes est une version différente (différente longueur, live, acoustique, remix, remasterisé, etc) que la version sur le Collection Metallica .

selon la façon dont vous voulez gérer (ou Ignorer) ce cas, vous pouvez soit suivre la route suggérée par beberlei, soit simplement suivre la logique supplémentaire proposée dans Album::getracklist(). Personnellement, je pense que la logique supplémentaire est justifiée pour garder votre API propre, mais les deux ont leur mérite.

si vous souhaitez accommoder mon cas d'utilisation, vous pouvez avoir des pistes contenant un référencement automatique de nombreuses autres pistes, éventuellement $ similarTracks. Dans ce cas, il y aurait deux entités pour la piste Batterie , une pour de Metallica Collection et une pour le Maître des Marionnettes . Chaque entité de piste similaire contiendrait alors une référence l'une à l'autre. De plus, cela éliminerait la classe AlbumTrackReference actuelle et éliminerait votre "problème"actuel. Je suis d'accord que c'est juste le déplacement de la complexité à un point différent, mais il est capable de gérer une usecase qu'il n'était pas capable de gérer auparavant.

7
répondu jsuggs 2010-09-29 21:50:07

demandez-Vous pour la "meilleure façon", mais il n'y a pas de meilleure façon. Il y a de nombreuses façons et vous en avez déjà découvert quelques-unes. La façon dont vous voulez gérer et/ou encapsuler la gestion d'association lors de l'utilisation des classes d'association dépend entièrement de vous et de votre domaine concret, personne ne peut vous montrer une "meilleure façon" je le crains.

en dehors de cela, la question pourrait être grandement simplifiée en supprimant la Doctrine et les bases de données relationnelles de l'équation. L'essence de votre question bout j'en viens à une question sur la façon de gérer les cours d'association dans le cadre de l'OOP ordinaire.

6
répondu romanb 2010-08-25 18:59:41

j'obtenais d'un conflit avec la table de jointure définie dans une classe d'association ( avec des champs personnalisés supplémentaires ) annotation et une table de jointure définie dans un beaucoup-à-plusieurs annotation.

les définitions de mappage dans deux entités avec une relation directe de plusieurs à plusieurs semblent se traduire par la création automatique de la table de jointure en utilisant l'annotation "joinTable". Cependant la table de jointure était déjà définie par une annotation dans sa classe d'entité sous-jacente et I voulait qu'il utilise les propres définitions de champs de cette classe d'association entity afin d'étendre la table de jointure avec des champs personnalisés supplémentaires.

l'explication et la solution sont celles indiquées par FMaz008 ci-dessus. Dans ma situation, c'était grâce à ce poste dans le forum ' question D'Annotation de Doctrine ". Ce billet attire l'attention sur la Documentation de Doctrine concernant ManyToMany Uni-directional relationships . Regarder la note en ce qui concerne l'approche consistant à utiliser une "classe d'entités d'association", remplaçant ainsi la mise en correspondance de plusieurs annotations directement entre deux classes d'entités principales par une annotation d'une à plusieurs dans les classes d'entités principales et deux annotations "de plusieurs à une" dans la classe d'entités associatives. Il y a un exemple fourni dans ce forum post modèles D'Association avec des champs supplémentaires :

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
répondu Ben 2013-10-03 13:57:15

cet exemple vraiment utile. Il manque dans la doctrine de la documentation 2.

Merci beaucoup.

pour les fonctions mandataires peut être fait:

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

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

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

et

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

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;
3
répondu Anthony 2010-10-29 00:26:45

vous faites référence à des métadonnées, des données sur les données. J'ai eu ce même problème pour le projet sur lequel je travaille actuellement et j'ai dû passer du temps à essayer de le résoudre. Il est trop d'informations à poster ici, mais ci-dessous sont deux liens que vous pouvez trouver utiles. Ils font référence au cadre de Symfony, mais sont basés sur la 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 /

bonne chance, et de belles références Metallica!

3
répondu Mike Purcell 2011-11-03 21:20:23

la solution se trouve dans la documentation de la Doctrine. Dans la FAQ vous pouvez voir ceci:

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

et le tutoriel est ici :

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

donc vous ne faites plus manyToMany mais vous devez créer une entité supplémentaire et mettre manyToOne à vos deux entités.

AJOUTER pour @f00bar commentaire :

c'est simple, il suffit de faire quelque chose comme ça:

Article  1--N  ArticleTag  N--1  Tag

donc vous créez une entité 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

j'espère que cela aide

3
répondu Mirza Selimovic 2015-03-23 13:53:27

unidirectionnel. Il suffit d'ajouter l'inversedBy:(nom de la colonne étrangère) pour la rendre bidirectionnelle.

# 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

j'espère que ça aidera. Voir vous.

3
répondu Gatunox 2015-05-27 03:40:58

, Vous pouvez être en mesure d'obtenir ce que vous voulez avec Classe de l'Héritage de Table où vous changez AlbumTrackReference à AlbumTrack:

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

et getTrackList() contiendraient AlbumTrack objets que vous pourriez alors utiliser comme vous voulez:

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

vous aurez besoin d'examiner ceci clairement pour s'assurer que vous ne souffrez pas de performance-sage.

votre configuration actuelle est simple, efficace, et facile à comprendre, même si certains aspects sémantiques ne vous conviennent pas.

2
répondu rojoca 2010-09-07 18:28:31

tout en obtenant toutes les pistes d'album forment à l'intérieur de la classe d'album, vous générerez une requête de plus pour un enregistrement de plus. C'est à cause de la méthode de remplacement. Il y a un autre exemple de mon code (voir dernier message dans le sujet): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

y a-t-il une autre méthode pour résoudre cela? Une seule jointure n'est-elle pas une meilleure solution?

0
répondu quba 2011-11-03 22:08:16

Voici la solution comme décrit dans le documentation 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
répondu medunes 2017-05-31 13:29:57