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.
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.
Une fois qu'une relation a des données, ce n'est plus une relation !
à 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.
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
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!
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.
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.
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;
}
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;
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!
la solution se trouve dans la documentation de la Doctrine. Dans la FAQ vous pouvez voir ceci:
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
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.
, 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.
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?
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();
}
}