Me pregunto cuál es la mejor, la forma más limpia y sencilla de trabajar con relaciones de muchos a muchos en Doctrine2.
Supongamos que tenemos un álbum como Master of Puppets de Metallica con varias pistas. Pero tenga en cuenta el hecho de que una pista puede aparecer en más de un álbum, como lo hace Battery by Metallica : tres álbumes presentan esta pista.
Entonces, lo que necesito es una relación de muchos a muchos entre álbumes y pistas, usando la tercera tabla con algunas columnas adicionales (como la posición de la pista en el álbum especificado). En realidad, tengo que usar, como sugiere la documentación de Doctrine, una doble relación de uno a muchos para lograr esa funcionalidad.
/** @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;
}
}
Data de muestra:
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 |
+----+----------+----------+----------+------------+
Ahora puedo mostrar una lista de álbumes y pistas asociadas a ellos:
$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!' : ''
);
}
}
Los resultados son lo que espero, es decir: una lista de álbumes con sus pistas en el orden apropiado y los promocionados que se marcan como promocionados.
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)
¿Así que qué hay de malo?
Este código demuestra lo que está mal:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
devuelve una matriz de AlbumTrackReference
objetos en lugar de Track
objetos. No puedo crear métodos proxy porque ¿qué pasa si ambos Album
y Track
tendrían getTitle()
método? Podría hacer un procesamiento adicional dentro del Album::getTracklist()
método, pero ¿cuál es la forma más sencilla de hacerlo? ¿Estoy obligado a escribir algo así?
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
EDITAR
@beberlei sugirió usar métodos proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Sería una buena idea, pero estoy usando ese "objeto de referencia" de ambos lados: $album->getTracklist()[12]->getTitle()
y $track->getAlbums()[1]->getTitle()
, por lo tanto, el getTitle()
método debería devolver datos diferentes según el contexto de invocación.
Tendría que hacer algo como:
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();
}
Y esa no es una forma muy limpia.
$album->getTracklist()[12]
esAlbumTrackRef
objeto, por$album->getTracklist()[12]->getTitle()
lo que siempre devolverá el título de la pista (si está utilizando el método proxy). Si bien$track->getAlbums()[1]
esAlbum
objeto, por$track->getAlbums()[1]->getTitle()
lo tanto , siempre se devolverá el título del álbum.AlbumTrackReference
dos métodos proxygetTrackTitle()
ygetAlbumTitle
.Respuestas:
Abrí una pregunta similar en la lista de correo de usuarios de Doctrine y obtuve una respuesta realmente simple;
considere la relación de muchos a muchos como una entidad en sí misma, y luego se da cuenta de que tiene 3 objetos, vinculados entre ellos con una relación de uno a muchos y de muchos a uno.
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Una vez que una relación tiene datos, ¡ya no es una relación!
fuente
app/console doctrine:mapping:import AppBundle yml
todavía genera una relación manyToMany para las dos tablas originales y simplemente ignora la tercera tabla en lugar de considerarla como una entidad:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
Cuál es la diferencia entre provisto por @Crozin yconsider the relationship as an entity
? Creo que lo que quiere preguntar es cómo omitir la entidad relacional y recuperar el título de una pista usandoforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
Desde $ album-> getTrackList () siempre obtendrá las entidades "AlbumTrackReference", entonces, ¿qué pasa con la adición de métodos desde el Track y el proxy?
De esta manera, su bucle se simplifica considerablemente, al igual que todos los demás códigos relacionados con el bucle de las pistas de un álbum, ya que todos los métodos solo se representan dentro de AlbumTrakcReference:
Por cierto, debe cambiar el nombre de AlbumTrackReference (por ejemplo, "AlbumTrack"). Claramente no es solo una referencia, sino que contiene lógica adicional. Dado que probablemente también hay pistas que no están conectadas a un álbum, sino que solo están disponibles a través de un CD promocional o algo así, esto también permite una separación más limpia.
fuente
Btw You should rename the AlbumT(...)
- Buen punto$album->getTracklist()[1]->getTrackTitle()
es tan bueno / malo como$album->getTracklist()[1]->getTrack()->getTitle()
. Sin embargo, parece que tendría que tener dos clases diferentes: una para referencias de álbum-> pista y otra para referencias de pista-> álbumes, y eso es demasiado difícil de implementar. Probablemente esa sea la mejor solución hasta ahora ...Nada mejor que un buen ejemplo
Para las personas que buscan un ejemplo de codificación limpia de una asociación uno a muchos / muchos a uno entre las 3 clases participantes para almacenar atributos adicionales en la relación, consulte este sitio:
buen ejemplo de asociaciones uno a muchos / muchos a uno entre las 3 clases participantes
Piensa en tus claves principales
También piense en su clave principal. A menudo puede usar claves compuestas para relaciones como esta. Doctrine apoya esto de forma nativa. Puede convertir sus entidades referenciadas en identificadores. Consulte la documentación sobre claves compuestas aquí
fuente
Creo que iría con la sugerencia de @ beberlei de usar métodos proxy. Lo que puede hacer para simplificar este proceso es definir dos interfaces:
Luego, tanto tu
Album
como tuTrack
pueden implementarlos, mientrasAlbumTrackReference
que aún pueden implementar ambos, de la siguiente manera:De esta manera, eliminando su lógica que hace referencia directa a a
Track
o anAlbum
, y simplemente reemplazándola para que use unTrackInterface
oAlbumInterface
, puede usar suAlbumTrackReference
en cualquier caso posible. Lo que necesitará es diferenciar un poco los métodos entre las interfaces.Esto no diferenciará el DQL ni la lógica del repositorio, pero sus servicios simplemente ignorarán el hecho de que está pasando una
Album
o unaAlbumTrackReference
, o unaTrack
o unaAlbumTrackReference
porque ha ocultado todo detrás de una interfaz :)¡Espero que esto ayude!
fuente
Primero, estoy de acuerdo con beberlei en sus sugerencias. Sin embargo, es posible que te estés diseñando en una trampa. Su dominio parece estar considerando que el título es la clave natural para una pista, lo que probablemente sea el caso del 99% de los escenarios con los que se encuentra. Sin embargo, ¿qué pasa si Battery on Master of the Puppets es una versión diferente (diferente duración, live, acústica, remix, remasterizada, etc.) que la versión de The Metallica Collection .
Dependiendo de cómo desee manejar (o ignorar) ese caso, puede seguir la ruta sugerida de beberlei o simplemente ir con su lógica adicional propuesta en Album :: getTracklist (). Personalmente, creo que la lógica adicional está justificada para mantener limpia su API, pero ambas tienen sus méritos.
Si desea acomodar mi caso de uso, puede hacer que las Pistas contengan un OneToMany autorreferenciado a otras Pistas, posiblemente $ Pistas similares. En este caso, habría dos entidades para la pista Battery , una para The Metallica Collection y otra para Master of the Puppets . Entonces cada entidad Track similar contendría una referencia entre sí. Además, eso eliminaría la clase actual AlbumTrackReference y eliminaría su "problema" actual. Estoy de acuerdo en que solo está moviendo la complejidad a un punto diferente, pero es capaz de manejar un caso de uso que antes no podía.
fuente
Pides la "mejor manera" pero no hay la mejor manera. Hay muchas formas y ya descubriste algunas de ellas. La forma en que desea administrar y / o encapsular la administración de asociaciones cuando usa clases de asociación depende totalmente de usted y de su dominio concreto, nadie puede mostrarle la "mejor manera", me temo.
Aparte de eso, la pregunta podría simplificarse mucho eliminando Doctrine y las bases de datos relacionales de la ecuación. La esencia de su pregunta se reduce a una pregunta sobre cómo lidiar con las clases de asociación en OOP simple.
fuente
Estaba obteniendo un conflicto con la tabla de unión definida en una anotación de clase de asociación (con campos personalizados adicionales) y una tabla de unión definida en una anotación de muchos a muchos.
Las definiciones de mapeo en dos entidades con una relación directa de muchos a muchos parecían dar como resultado la creación automática de la tabla de combinación mediante la anotación 'joinTable'. Sin embargo, la tabla de unión ya estaba definida por una anotación en su clase de entidad subyacente y quería que usara las propias definiciones de campo de esta clase de entidad de asociación para extender la tabla de unión con campos personalizados adicionales.
La explicación y solución es la identificada por FMaz008 arriba. En mi situación, fue gracias a esta publicación en el foro ' Doctrine Annotation Question '. Esta publicación llama la atención sobre la documentación de Doctrine con respecto a las relaciones unidireccionales ManyToMany . Mire la nota con respecto al enfoque de usar una 'clase de entidad de asociación', reemplazando así el mapeo de anotaciones de muchos a muchos directamente entre dos clases de entidades principales con una anotación de uno a muchos en las clases de entidades principales y dos 'muchos a -una 'anotaciones en la clase de entidad asociativa. Hay un ejemplo proporcionado en esta publicación del foro Modelos de asociación con campos adicionales :
fuente
Este ejemplo realmente útil. Carece de documentación en la doctrina 2.
Muchas gracias.
Para las funciones de proxy se pueden hacer:
y
fuente
A lo que se refiere es a metadatos, datos sobre datos. Tuve este mismo problema para el proyecto en el que estoy trabajando actualmente y tuve que pasar algún tiempo tratando de resolverlo. Es demasiada información para publicar aquí, pero a continuación hay dos enlaces que pueden resultarle útiles. Hacen referencia al marco de Symfony, pero se basan en 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/
¡Buena suerte y buenas referencias de Metallica!
fuente
La solución está en la documentación de Doctrine. En las preguntas frecuentes puede ver esto:
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
Y el tutorial está aquí:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
Entonces ya no haces un
manyToMany
pero tienes que crear una Entidad extra y ponermanyToOne
a tus dos entidades.AGREGAR para el comentario de @ f00bar:
es simple, solo tienes que hacer algo como esto:
Entonces creas una entidad ArticleTag
Espero que ayude
fuente
:(
¿Alguien podría compartir un ejemplo del tercer caso de uso usando el formato yml? Realmente me appriace:#
Unidireccional Simplemente agregue el invertedBy: (Nombre de columna extranjera) para hacerlo bidireccional.
Espero que ayude. Nos vemos.
fuente
Es posible que pueda lograr lo que desea con Herencia de tabla de clase donde cambia AlbumTrackReference a AlbumTrack:
Y
getTrackList()
contendríaAlbumTrack
objetos que luego podría usar como desee:Tendrá que examinar esto a fondo para asegurarse de que no sufra en cuanto al rendimiento.
Su configuración actual es simple, eficiente y fácil de entender, incluso si algunas de las semánticas no le sientan bien.
fuente
Mientras obtiene todas las pistas del álbum dentro de la clase de álbum, generará una consulta más para un registro más. Eso se debe al método proxy. Hay otro ejemplo de mi código (vea la última publicación en el tema): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
¿Hay algún otro método para resolver eso? ¿No es una sola combinación una mejor solución?
fuente
Aquí está la solución como se describe en la documentación de Doctrine2
fuente