Je me demande quelle est la meilleure, la plus propre et la plus simple façon de travailler avec des relations plusieurs-à-plusieurs dans Doctrine2.
Supposons que nous ayons un album comme Master of Puppets de Metallica avec plusieurs pistes. Mais veuillez noter le fait qu'une piste peut apparaître dans plus d'un album, comme le fait Battery by Metallica - trois albums présentent cette piste.
Donc, ce dont j'ai besoin, c'est d'une relation plusieurs-à-plusieurs entre les albums et les pistes, en utilisant un 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 le suggère la documentation de Doctrine, 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 \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;
}
}
Exemples de données:
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 d'albums et de pistes qui leur sont associés:
$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!' : ''
);
}
}
Les résultats sont ce que j'attends, c'est-à-dire: une liste d'albums avec leurs morceaux dans l'ordre approprié et ceux promus é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()
renvoie un tableau d' AlbumTrackReference
objets au lieu d' Track
objets. Je ne peux pas créer de méthodes proxy car si les deux, Album
et Track
aurait une getTitle()
méthode? Je pourrais faire un traitement supplémentaire dans la Album::getTracklist()
méthode, mais quelle est la façon la plus simple de le faire? Suis-je obligé d'é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
ÉDITER
@beberlei a suggéré d'utiliser des méthodes proxy:
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()
, donc, la getTitle()
méthode devrait renvoyer des données différentes en fonction du contexte de l'invocation.
Je devrais 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 une façon très propre.
$album->getTracklist()[12]
isAlbumTrackRef
object, donc$album->getTracklist()[12]->getTitle()
retournera toujours le titre de la piste (si vous utilisez la méthode proxy). Bien qu'il$track->getAlbums()[1]
soitAlbum
objet, il$track->getAlbums()[1]->getTitle()
renverra toujours le titre de l'album.AlbumTrackReference
deux méthodes proxy,getTrackTitle()
etgetAlbumTitle
.Réponses:
J'ai ouvert une question similaire dans la liste de diffusion des utilisateurs Doctrine et j'ai obtenu une réponse très simple;
considérez la relation plusieurs à plusieurs comme une entité elle-même, puis vous réalisez que vous avez 3 objets, liés entre eux avec une relation un-à-plusieurs et 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!
la source
app/console doctrine:mapping:import AppBundle yml
génère toujours la relation manyToMany pour les deux tables d'origine et ignore simplement la troisième table au lieu de la considérer comme une entité:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
fourni par @Crozin etconsider the relationship as an entity
? Je pense que ce qu'il veut demander, c'est comment sauter l'entité relationnelle et récupérer le titre d'une piste en utilisantforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
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?
De cette façon, votre boucle se simplifie considérablement, ainsi que tout autre code lié à la boucle des pistes d'un album, car toutes les méthodes sont simplement mandatées dans AlbumTrakcReference:
Btw Vous devez renommer AlbumTrackReference (par exemple "AlbumTrack"). Ce n'est clairement pas seulement une référence, mais contient une logique supplémentaire. Puisqu'il y a probablement aussi des pistes qui ne sont pas connectées à un album mais simplement disponibles via un cd promo ou quelque chose cela permet également une séparation plus nette.
la source
Btw You should rename the AlbumT(...)
- bon point$album->getTracklist()[1]->getTrackTitle()
est aussi bon / mauvais que$album->getTracklist()[1]->getTrack()->getTitle()
. Cependant, il semble que je devrais avoir deux classes différentes: une pour les références d'album-> pistes et une autre pour les références de pistes-> albums - et c'est trop difficile à mettre en œuvre. C'est probablement la meilleure solution jusqu'à présent ...Rien ne vaut un bel exemple
Pour les personnes à la recherche d'un exemple de codage propre d'une association un-à-plusieurs / plusieurs-à-un entre les 3 classes participantes pour stocker des attributs supplémentaires dans la relation, consultez ce site:
bel exemple d'associations un-à-plusieurs / plusieurs-à-un entre les 3 classes participantes
Pensez à vos clés primaires
Pensez également à votre clé primaire. Vous pouvez souvent utiliser des clés composites pour des relations comme celle-ci. La doctrine le soutient nativement. Vous pouvez transformer vos entités référencées en identifiants. Consultez la documentation sur les clés composites ici
la source
Je pense que j'irais avec la suggestion de @ beberlei d'utiliser des méthodes de proxy. Pour simplifier ce processus, vous pouvez définir deux interfaces:
Ensuite, votre
Album
et votreTrack
pouvez les implémenter, tandis que leAlbumTrackReference
peut toujours implémenter les deux, comme suit:De cette façon, en supprimant votre logique qui fait directement référence à un
Track
ou unAlbum
, et en la remplaçant simplement pour qu'elle utilise unTrackInterface
ouAlbumInterface
, vous pouvez utiliser votreAlbumTrackReference
dans tous les cas possibles. Ce dont vous aurez besoin est de différencier un peu les méthodes entre les interfaces.Cela ne différenciera pas le DQL ni la logique du référentiel, mais vos services ignoreront simplement le fait que vous passez un
Album
ou unAlbumTrackReference
, ou unTrack
ou unAlbumTrackReference
parce que vous avez tout caché derrière une interface :)J'espère que cela t'aides!
la source
Tout d'abord, je suis principalement d'accord avec beberlei sur ses suggestions. Cependant, vous vous concevez peut-être 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. Et si Battery on Master of the Puppets est une version différente (longueur différente, live, acoustique, remix, remasterisé, etc.) que la version de The Metallica Collection .
Selon la façon dont vous voulez gérer (ou ignorer) ce cas, vous pouvez soit suivre l'itinéraire suggéré par beberlei, soit simplement utiliser la logique supplémentaire proposée dans Album :: getTracklist (). 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 prendre en compte mon cas d'utilisation, vous pourriez avoir des pistes contenant un OneToMany auto-référencé avec d'autres pistes, éventuellement $ similarTracks. Dans ce cas, il y aurait deux entités pour la piste Battery , une pour The Metallica Collection et une pour Master of the Puppets . Ensuite, chaque entité Piste similaire contiendrait une référence l'une à l'autre. En outre, cela éliminerait la classe AlbumTrackReference actuelle et éliminerait votre "problème" actuel. Je suis d'accord qu'il déplace simplement la complexité vers un point différent, mais il est capable de gérer un cas d'utilisation qu'il n'était pas en mesure auparavant.
la source
Vous demandez le "meilleur moyen" mais il n'y a pas de meilleur moyen. Il existe de nombreuses façons et vous en avez déjà découvert certaines. La façon dont vous voulez gérer et / ou encapsuler la gestion des associations lorsque vous utilisez des classes d'association dépend entièrement de vous et de votre domaine concret, personne ne peut vous montrer la "meilleure façon", je le crains.
En dehors de cela, la question pourrait être beaucoup simplifiée en supprimant la doctrine et les bases de données relationnelles de l'équation. L'essence de votre question se résume à une question sur la façon de traiter les classes d'association dans la POO ordinaire.
la source
J'obtenais un conflit avec une table de jointure définie dans une annotation de classe d'association (avec des champs personnalisés supplémentaires) et une table de jointure définie dans une annotation plusieurs à plusieurs.
Les définitions de mappage dans deux entités avec une relation directe plusieurs-à-plusieurs semblaient entraîner la création automatique de la table de jointure à l'aide de l'annotation «joinTable». Cependant, la table de jointure était déjà définie par une annotation dans sa classe d'entité sous-jacente et je voulais qu'elle utilise les propres définitions de champ de cette classe d'entité d'association afin d'étendre la table de jointure avec des champs personnalisés supplémentaires.
L'explication et la solution sont celles identifiées par FMaz008 ci-dessus. Dans ma situation, c'était grâce à ce post dans le forum ' Doctrine Annotation Question '. Ce message attire l'attention sur la documentation de la Doctrine concernant les relations unidirectionnelles ManyToMany . Regardez la note concernant l'approche de l'utilisation d'une `` classe d'entités d'association '', remplaçant ainsi le mappage d'annotations plusieurs à plusieurs directement entre deux classes d'entités principales par une annotation un à plusieurs dans les classes d'entités principales et deux 'plusieurs à plusieurs'. -une 'annotation dans la classe d'entité associative. Il y a un exemple fourni dans ce forum post Modèles d'association avec des champs supplémentaires :
la source
Cet exemple vraiment utile. Il manque dans la doctrine de la documentation 2.
Merci beaucoup.
Pour les fonctions de proxy peuvent être effectuées:
et
la source
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 un peu de temps à essayer de le comprendre. C'est trop d'informations à publier ici, mais voici deux liens qui peuvent vous être utiles. Ils font référence au framework Symfony, mais sont basés sur l'ORM Doctrine.
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 belles références Metallica!
la source
La solution se trouve dans la documentation de 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 mettremanyToOne
dans vos deux entités.AJOUTER pour le commentaire @ f00bar:
c'est simple, il suffit de faire quelque chose comme ça:
Vous créez donc une entité ArticleTag
J'espère que ça aide
la source
:(
Quelqu'un pourrait-il partager un exemple du troisième cas d'utilisation en utilisant le format yml? J'apprécierais vraiment:#
Unidirectionnel. Ajoutez simplement inversedBy: (nom de la colonne étrangère) pour le rendre bidirectionnel.
J'espère que ça aide. À plus.
la source
Vous pourrez peut-être réaliser ce que vous voulez avec l' héritage de table de classe où vous changez AlbumTrackReference en AlbumTrack:
Et
getTrackList()
contiendrait desAlbumTrack
objets que vous pourriez ensuite utiliser comme vous le souhaitez:Vous devrez l'examiner attentivement pour vous assurer de ne pas souffrir en termes de performances.
Votre configuration actuelle est simple, efficace et facile à comprendre même si certaines sémantiques ne vous conviennent pas.
la source
Lors de la création de toutes les pistes d'album dans la classe d'album, vous générez une requête de plus pour un enregistrement de plus. C'est à cause de la méthode proxy. Il y a un autre exemple de mon code (voir le dernier message dans la rubrique): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Existe-t-il une autre méthode pour résoudre ce problème? Une seule jointure n'est-elle pas une meilleure solution?
la source
Voici la solution décrite dans la documentation Doctrine2
la source