Je me demande quelle est la meilleure façon, la plus propre et la plus simple de travailler avec plusieurs relations dans Doctrine2.
Supposons que nous ayons un album du type Master of Puppets de Metallica avec plusieurs pistes. Mais notez s'il vous plaît qu’une piste peut apparaître dans plusieurs albums, comme Battery de Metallica fait - trois albums sont mettant en vedette cette piste.
Donc, ce dont j'ai besoin, c'est d'une relation multiple entre albums et 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 obtenir 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;
}
}
Échantillon 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 des albums et des 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 à quoi je m'attendais, à savoir: une liste d'albums avec leurs pistes dans l'ordre approprié et les albums promus 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)
Ce code montre ce qui ne va pas:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
renvoie un tableau d'objets AlbumTrackReference
au lieu d'objets Track
. Je ne peux pas créer de méthodes proxy car si Album
et Track
avaient la méthode getTitle()
? Je pourrais faire un traitement supplémentaire dans la méthode Album::getTracklist()
, mais quel est le moyen le 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
@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 méthode getTitle()
devrait renvoyer des données différentes en fonction du contexte d'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.
J'ai ouvert une question similaire dans la liste de diffusion 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éaliserez que vous avez 3 objets, liés entre eux par une relation un-à-plusieurs et plusieurs-à-un.
Une fois qu'une relation a des données, ce n'est plus une relation!
À partir de $ album-> getTrackList (), vous obtiendrez toujours des entités "AlbumTrackReference", alors pourquoi ne pas ajouter des 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 se simplifie considérablement, ainsi que tous les autres codes liés à la boucle des pistes d’un album, puisque toutes les méthodes sont simplement proxy dans 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 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 morceaux qui ne sont pas connectés à un album mais juste disponibles via un cd promo ou quelque chose qui permet une séparation plus nette également.
Rien ne vaut un bel exemple
Pour les personnes à la recherche d'un exemple de codage propre d'associations un à plusieurs/plusieurs à un entre les 3 classes participantes pour stocker des attributs supplémentaires dans la relation, consultez ce site:
Bel exemple d'association un-plusieurs-plusieurs/plusieurs-pour-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. Doctrine supporte cela de manière native. Vous pouvez transformer vos entités référencées en identifiants. Consultez la documentation sur les clés composites ici
Je pense que j'accepterais la suggestion de @ beberlei d'utiliser des méthodes de proxy. Pour simplifier ce processus, vous pouvez définir deux interfaces:
interface AlbumInterface {
public function getAlbumTitle();
public function getTracklist();
}
interface TrackInterface {
public function getTrackTitle();
public function getTrackDuration();
}
Ensuite, votre Album
et votre Track
peuvent les implémenter, alors que le AlbumTrackReference
peut toujours implémenter 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 référençant directement une Track
ou une Album
, et en la remplaçant simplement pour qu'elle utilise un TrackInterface
ou AlbumInterface
, vous utiliserez votre AlbumTrackReference
dans tous les cas possibles. Ce dont vous aurez besoin, c'est de différencier un peu les méthodes entre les interfaces.
Cela ne différenciera pas la logique DQL ni celle du référentiel, mais vos services ignoreront simplement le fait que vous transmettez un Album
ou un AlbumTrackReference
, ou un Track
ou un AlbumTrackReference
parce que vous avez tout caché derrière une interface :)
J'espère que cela t'aides!
Premièrement, je suis surtout d’accord avec beberlei sur ses suggestions. Cependant, vous pouvez vous concevoir comme un piège. Votre domaine semble considérer que le titre est la clé naturelle d'une piste, ce qui est probablement le cas pour 99% des scénarios que vous rencontrez. Cependant, que faire si Battery sur Master of the Puppets est une version différente (durée, live, acoustique, remix, remasterisé, etc.) de la version de La collection Metallica.
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 aller avec votre 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 charge mon cas d'utilisation, vous pouvez faire en sorte que les pistes contiennent un auto-référence OneToMany avec d'autres pistes, éventuellement $ similarTracks. Dans ce cas, il y aurait deux entités pour la piste Batterie, une pour La collection Metallica et une pour Maître des marionnettes. Chaque entité de piste similaire contiendrait alors une référence les unes aux autres. En outre, cela éliminerait la classe actuelle AlbumTrackReference et éliminerait votre "problème" actuel. Je conviens qu'il s'agit simplement de déplacer la complexité vers un point différent, mais il est capable de gérer un cas d'utilisation auquel il n'était pas capable auparavant.
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 manière dont vous souhaitez gérer et/ou encapsuler la gestion des associations lors de l'utilisation de classes d'association dépend de votre domaine et de votre choix. Personne ne peut vous montrer le "meilleur moyen", j'en ai bien peur.
En dehors de cela, la question pourrait être beaucoup simplifiée en supprimant 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 en langage OOP simple.
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 ont apparemment abouti à 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 souhaitais 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 message dans le forum ' Doctrine Annotation Question '. Cet article attire l’attention sur la Doctrine documentation concernant Plusieurs relations unidirectionnelles . Observez la remarque concernant l'approche consistant à utiliser 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". -one 'annotations dans la classe d'entités associatives. Un exemple est fourni dans cet article de forum 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;
}
La solution est dans la documentation de 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 de manyToMany
mais vous devez créer une entité supplémentaire et mettre manyToOne
dans vos deux entités.
AJOUTE pour le commentaire @ f00bar:
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 ça aide
Cet exemple vraiment utile. Il manque dans la documentation doctrine 2.
Merci beaucoup.
Pour les mandataires, les fonctions peuvent être réalisées:
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;
Unidirectionnel. Ajoutez simplement l'inverse de: (Nom de la colonne étrangère) pour le rendre bidirectionnel.
# 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 aide. À plus.
Vous parlez de métadonnées, de données sur les données. J'ai eu le même problème pour le projet sur lequel je travaille actuellement et j'ai dû passer du temps à essayer de le comprendre. C’est trop d’informations à publier ici, mais vous trouverez ci-dessous deux liens utiles. Ils font référence au framework Symfony, mais sont basés sur le 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 références Metallica!
Vous pourrez peut-être réaliser ce que vous voulez avec Class Table Hheritance en modifiant AlbumTrackReference en AlbumTrack:
class AlbumTrack extends Track { /* ... */ }
Et getTrackList()
contiendrait AlbumTrack
objets que vous pourriez ensuite utiliser comme vous le souhaitez:
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 devrez examiner cela à fond pour vous assurer de ne pas nuire à la performance.
Votre configuration actuelle est simple, efficace et facile à comprendre, même si certaines sémantiques ne vous conviennent pas.
Tout en obtenant toutes les pistes de l'album dans la classe d'albums, vous allez générer une requête supplémentaire pour un enregistrement supplémentaire. C'est à cause de la méthode du proxy. Voici un autre exemple de code (voir le dernier message de 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?
Voici la solution décrite dans le Documentation de 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();
}
}