tutorial generate framework español cli php orm model doctrine doctrine2

php - generate - Doctrine2: la mejor manera de manejar muchos a muchos con columnas adicionales en la tabla de referencia



install doctrine orm (14)

Me pregunto cuál es la mejor, la forma más limpia y sencilla de trabajar con las relaciones de muchos a muchos en Doctrine2.

Supongamos que tenemos un álbum como Master of Puppets de Metallica con varias canciones. Pero tenga en cuenta el hecho de que una pista puede aparecer en más de un álbum, como lo hace Battery by Metallica ; hay tres álbumes con esta canción.

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 un álbum específico) En realidad, tengo que usar, como sugiere la documentación de Doctrine, una relación doble 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 estoy esperando, es decir: una lista de álbumes con sus canciones en el orden apropiado y los promocionados 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 objetos AlbumTrackReference lugar de objetos Track . No puedo crear métodos proxy porque, ¿si ambos, Album y Track tendrían el método getTitle() ? Podría hacer un procesamiento adicional dentro del método Album::getTracklist() 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 getTitle() método getTitle() debe 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.


A lo que te refieres son metadatos, datos sobre datos. Tuve este mismo problema para el proyecto en el que estoy trabajando actualmente y tuve que pasar un tiempo tratando de resolverlo. Es demasiada información para publicar aquí, pero a continuación hay dos enlaces que pueden ser útiles. Hacen referencia al marco de Symfony, pero se basan en el ORM de 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/

Buena suerte, y bonitas referencias de Metallica!



Aquí está la solución como se describe en la documentación 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(); } }


Creo que me gustaría seguir con la sugerencia de @ beberlei de usar métodos proxy. Lo que puede hacer para simplificar este proceso es definir dos interfaces:

interface AlbumInterface { public function getAlbumTitle(); public function getTracklist(); } interface TrackInterface { public function getTrackTitle(); public function getTrackDuration(); }

Luego, tanto tu Album como tu Track pueden implementarlos, mientras que la AlbumTrackReference todavía puede implementar ambos, como sigue:

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 esta manera, al eliminar su lógica que hace referencia directamente a una Track o un Album , y simplemente reemplazarla para que use un TrackInterface o TrackInterface , podrá usar su AlbumTrackReference en cualquier caso posible. Lo que necesitará es diferenciar un poco los métodos entre las interfaces.

Esto no diferenciará la lógica del DQL ni del repositorio, pero sus servicios simplemente ignorarán el hecho de que está pasando un Album o una Track Album , o una Track o una AlbumTrackReference Track Album porque ha ocultado todo detrás de una interfaz :)

¡Espero que esto ayude!


Desde $ album-> getTrackList () siempre obtendrás las entidades "AlbumTrackReference" de vuelta, así que, ¿qué hay de agregar métodos desde la pista y el proxy?

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

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

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 pero que solo están disponibles a través de un promo-cd o algo que también permita una separación más limpia.


En primer lugar, estoy más de acuerdo con beberlei en sus sugerencias. Sin embargo, puedes estar diseñándote en una trampa. Su dominio parece estar considerando que el título es la clave natural para una pista, lo que probablemente es el caso del 99% de los escenarios que se presentan. Sin embargo, ¿qué pasa si Battery on Master of the Puppets es una versión diferente (diferente duración, en vivo, acústica, remezcla, remasterizada, etc.) que la versión de The Metallica Collection ?

Dependiendo de cómo quiera manejar (o ignorar) ese caso, puede ir por 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 su API limpia, pero ambas tienen su mérito.

Si desea acomodar mi caso de uso, puede hacer que las pistas contengan una referencia automática de OneToMany a otras pistas, posiblemente $ pistas similares. En este caso, habría dos entidades para la batería de la pista, una para The Metallica Collection y otra para Master of the Puppets . Entonces, cada entidad de pista similar contendría una referencia entre sí. Además, eso eliminaría la clase actual de AlbumTrackReference y eliminaría su "problema" actual. Estoy de acuerdo en que simplemente está moviendo la complejidad a un punto diferente, pero es capaz de manejar un caso de uso que antes no podía.


Es posible que pueda lograr lo que quiere con la herencia de tabla de clase en la que cambia AlbumRackReference a AlbumTrack:

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

Y getTrackList() contendría objetos AlbumTrack que luego podría usar como quiera:

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

Tendrá que examinar esto a fondo para asegurarse de que no sufre el rendimiento.

Su configuración actual es simple, eficiente y fácil de entender, incluso si parte de la semántica no le sienta bien.


Este ejemplo realmente útil. Falta en la documentación de la doctrina 2.

Muchas gracias.

Para las funciones proxies se pueden hacer:

class AlbumTrack extends AlbumTrackAbstract { ... proxy method. function getTitle() {} } class TrackAlbum extends AlbumTrackAbstract { ... proxy method. function getTitle() {} } class AlbumTrackAbstract { private $id; .... }

y

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



La solución está en la documentación de Doctrine. En el FAQ puedes 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

Por lo tanto, ya no hace manyToMany pero tiene que crear una entidad adicional y poner manyToOne a manyToOne a sus dos entidades.

AÑADIR para el comentario @ f00bar:

Es simple, solo tienes que hacer algo como esto:

Article 1--N ArticleTag N--1 Tag

Así que creas una entidad 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

Espero que ayude


Obtuve de un conflicto con una tabla de unión definida en una clase de asociación (con campos personalizados adicionales) una anotación 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 lugar a la creación automática de la tabla de unión utilizando 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 definiciones de campo propias de esta clase de entidad de asociación para ampliar 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 este post en el foro '' Doctrine Annotation Question ''. Esta publicación llama la atención sobre la documentación de Doctrine sobre las relaciones uni-direccionales de ManyToMany . Mire la nota sobre el enfoque de usar una ''clase de entidad de asociación'', sustituyendo así el mapeo de anotación 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 muchos -una anotación en la clase de entidad asociativa. Hay un ejemplo proporcionado en este foro. Modelos de asociación con campos adicionales :

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


Pides la "mejor manera" pero no hay mejor manera. Hay muchas formas y ya has descubierto algunas de ellas. La forma en que desee administrar y / o encapsular la administración de la asociación cuando utilice clases de asociación depende totalmente de usted y su dominio concreto, nadie puede mostrarle una "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 tratar con las clases de asociación en OOP simple.


Unidireccional Solo agregue el inversedBy: (Nombre de la columna extranjera) para hacerlo bidireccional.

# 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

Espero que ayude. Nos vemos.


Nada mejor que un buen ejemplo

Para las personas que buscan un ejemplo de codificación limpia de asociaciones de uno a muchos / muchos a uno entre las 3 clases participantes para almacenar atributos adicionales en la relación, visite este sitio:

buen ejemplo de asociaciones de uno a muchos / muchos a uno entre las 3 clases participantes

Piensa en tus llaves primarias

También piensa en tu clave principal. A menudo se pueden usar claves compuestas para relaciones como esta. La doctrina apoya esto de forma nativa. Puedes convertir tus entidades referenciadas en ids. Consulta la documentación sobre claves compuestas aquí.