282 votos

Doctrine2: Mejor manera de manejar muchos-a-muchos con más columnas de la tabla de referencia

Me pregunto ¿cuál es la mejor, la más limpia y la más simple manera de trabajar con muchos-a-muchos de relaciones en Doctrine2.

Supongamos que tenemos un disco como Master of Puppets de Metallica con varias pistas. Pero tenga en cuenta el hecho de que una pista puede aparece en más que un álbum, como el Batería de Metallica hace - tres son los álbumes con este tema.

Así que lo que necesito es de muchos a muchos relación entre álbumes y canciones, el uso de terceros de la tabla con algunas columnas adicionales (como la posición de la pista especificada en el álbum). En realidad tengo que usar, como la Doctrina de la documentación sugiere, un doble de uno a muchos relación para lograr que la 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;
    }
}

Datos de ejemplo:

             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 ver una lista de los álbumes y las 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 yo esperaba, es decir: una lista de los álbumes con sus pistas en el orden correcto y promovido las que están marcadas como promueve.

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) 

Entonces, ¿qué tiene de malo?

Este código muestra 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. Que no se puede crear proxy métodos de causa lo que si ambos, Album y Track habría getTitle() método? Yo podría hacer algún procesamiento adicional en Album::getTracklist() método, pero ¿cuál es la más simple manera de hacer eso? ¿Estoy obligado a hacer escribir algo como eso?

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 sugiere utilizar los métodos del proxy:

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

Que sería una buena idea, pero yo estoy usando la de que "el objeto de referencia" de ambos lados: $album->getTracklist()[12]->getTitle() y $track->getAlbums()[1]->getTitle(), por lo que getTitle() método debe devolver datos diferentes basados en el contexto de la invocación.

Yo 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 eso no es muy limpio.

158voto

FMaz008 Puntos 3389

He abierto una pregunta similar en la Doctrina de la lista de correo de usuarios y tiene un realmente simple respuesta;

considerar la relación de muchos a muchos, como una entidad en sí misma, y luego te das cuenta de que tienes 3 objetos, conectados entre ellos con un uno-a-muchos y muchos-a-una relación.

http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Una vez que una relación ha de datos, no es más que una relación !

17voto

beberlei Puntos 2645

Desde $album->getTrackList() se alwas conseguir "AlbumTrackReference" entidades de vuelta, entonces, ¿qué acerca de la adición de los métodos de la Pista y los proxy?

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

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

De esta manera el bucle se simplifica considerablemente, así como todos los otros códigos relacionados con el bucle de las pistas de un álbum, ya que todos los métodos son sólo proxy 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 Que debería cambiar el nombre de la AlbumTrackReference (por ejemplo, "AlbumTrack"). Claramente no es sólo una referencia, pero contiene una lógica adicional. Puesto que hay probablemente también las Pistas que no están conectados a un álbum, pero sólo disponibles a través de un promo cd o algo esto permite una separación más clara también.

13voto

Wilt Puntos 867

Nada mejor que un buen ejemplo

Para las personas que buscan un lugar limpio ejemplo de codificación de un uno-a-muchos/muchos-a-uno asociaciones entre el 3 clases participantes para almacenar los atributos extra en la relación visite este sitio web:

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

Piense acerca de sus claves principales

Piense también acerca de su clave primaria. A menudo puedes usar claves compuestas por relaciones como esta. La doctrina admite de forma nativa este. Usted puede hacer que su referencia a las entidades en los identificadores. Consulte la documentación aquí: clic

7voto

jsuggs Puntos 1038

En primer lugar, que en su mayoría están de acuerdo con beberlei a sus sugerencias. Sin embargo, puede diseñar usted mismo en una trampa. Su dominio parece ser teniendo en cuenta el título de ser la clave natural para una pista, que es probablemente el caso de que el 99% de las situaciones que te encuentres. Sin embargo, ¿qué ocurre si la Batería en el Maestro de las Marionetas es una versión diferente (diferente longitud, en vivo, acústico, remix, remasterizado, etc) que la versión en La Metallica de la Colección.

Dependiendo de cómo desea controlar (o ignorar) en ese caso, usted podría ir beberlei la ruta sugerida, o simplemente vaya con su propuesta lógica adicional en Disco::getTracklist(). Personalmente, creo que la lógica adicional se justifica para mantener su API limpio, pero ambos tienen sus méritos.

Si usted desea acomodar mi caso de uso, usted podría tener Pistas de contener una auto referencia OneToMany a otras Pistas, posiblemente $similarTracks. En este caso, habría dos entidades para la pista de Batería, uno de Los Metallica de la Colección y uno para el Maestro de las Marionetas. A continuación, cada similares Pista entidad podría contener una referencia a cada uno de los otros. También, que sería deshacerse de la actual AlbumTrackReference de clase y de eliminar su actual "problema". Estoy de acuerdo en que es solo el movimiento de la complejidad a un punto diferente, pero es capaz de manejar un caso de uso que no estaba anteriormente.

6voto

romanb Puntos 2602

Pregunta por el "mejor camino", pero no hay mejor manera. Hay muchas maneras y has descubierto ya algunos de ellos. ¿Cómo desea administrar y/o encapsular la gerencia de la asociación cuando se utiliza asociación de las clases es totalmente de usted y su dominio concreto, nadie puede mostrar una "mejor manera" tengo miedo.

Aparte de eso, la pregunta podría ser simplificado mucho por la eliminación de la Doctrina y de las bases de datos relacionales a partir de la ecuación. La esencia de su pregunta se reduce a una pregunta acerca de cómo lidiar con la asociación de las clases en formato de programación orientada a objetos.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X