Pergunto-me qual é a melhor, a maneira mais limpa e mais simples de trabalhar com relações muitos-para-muitos no Doctrine2.
Vamos supor que temos um álbum como o Master of Puppets do Metallica com várias faixas. Mas observe o fato de que uma faixa pode aparecer em mais de um álbum, como o Battery by Metallica - três álbuns estão apresentando essa faixa.
Então, o que eu preciso é de um relacionamento muitos para muitos entre álbuns e faixas, usando a terceira tabela com algumas colunas adicionais (como a posição da faixa no álbum especificado). Na verdade, tenho que usar, como sugere a documentação de Doctrine, uma dupla relação um-para-muitos para alcançar essa funcionalidade.
/** @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;
}
}
Dados de amostra:
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 |
+----+----------+----------+----------+------------+
Agora eu posso exibir uma lista de álbuns e faixas associadas a eles:
$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!' : ''
);
}
}
Os resultados são o que eu estou esperando, ou seja: uma lista de álbuns com suas faixas na ordem apropriada e os promovidos sendo marcados como promovidos.
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)
Então, oque há de errado?
Este código demonstra o que está errado:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
devolve uma disposição de AlbumTrackReference
objectos em vez de Track
objectos. Eu não posso criar métodos de proxy porque e se ambos, Album
e Track
teria getTitle()
método? Eu poderia fazer algum processamento extra dentro do Album::getTracklist()
método, mas qual é a maneira mais simples de fazer isso? Sou forçado a escrever algo assim?
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 sugeriu usar métodos de proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Isso seria uma boa idéia, mas eu estou usando esse "objeto de referência" de ambos os lados: $album->getTracklist()[12]->getTitle()
e $track->getAlbums()[1]->getTitle()
, por isso getTitle()
método deve retornar dados diferentes com base no contexto de invocação.
Eu teria que fazer 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();
}
E essa não é uma maneira muito limpa.
$album->getTracklist()[12]
éAlbumTrackRef
object, portanto$album->getTracklist()[12]->getTitle()
, sempre retornará o título da faixa (se você estiver usando o método proxy). Enquanto$track->getAlbums()[1]
éAlbum
objeto,$track->getAlbums()[1]->getTitle()
sempre retornará o título do álbum.AlbumTrackReference
dois métodos de proxy,getTrackTitle()
egetAlbumTitle
.Respostas:
Abri uma pergunta semelhante na lista de discussão de usuários do Doctrine e recebi uma resposta muito simples;
considere a relação muitos para muitos como uma entidade em si, e então você percebe que possui três objetos, vinculados entre eles com uma relação um para muitos e muitos para um.
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Uma vez que uma relação tenha dados, não será mais uma relação!
fonte
app/console doctrine:mapping:import AppBundle yml
ainda geram relação ManyToMany para as duas tabelas originais e simplesmente ignorar a terceira tabela, em vez de concidering-lo como uma entidade:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
fornecido pelo @Crozin econsider the relationship as an entity
? Eu acho que o que ele quer fazer é como pular a entidade relacional e recuperar o título de uma faixa utilizandoforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
Em $ album-> getTrackList (), você também recebe entidades "AlbumTrackReference" de volta. E quanto a adicionar métodos da faixa e do proxy?
Dessa forma, seu loop simplifica consideravelmente, assim como todos os outros códigos relacionados ao loop das faixas de um álbum, já que todos os métodos são submetidos a proxy dentro do AlbumTrakcReference:
Você deve renomear o AlbumTrackReference (por exemplo "AlbumTrack"). É claramente não apenas uma referência, mas contém lógica adicional. Como provavelmente existem também faixas que não estão conectadas a um álbum, mas disponíveis apenas através de um CD promocional ou algo que permita uma separação mais limpa também.
fonte
Btw You should rename the AlbumT(...)
- bom ponto$album->getTracklist()[1]->getTrackTitle()
é tão bom / ruim quanto$album->getTracklist()[1]->getTrack()->getTitle()
. No entanto, parece que eu teria que ter duas classes diferentes: uma para referências de álbuns -> faixas e outra para referências de faixas -> álbuns - e isso é muito difícil de implementar. Então, provavelmente essa é a melhor solução até agora ...Nada supera um bom exemplo
Para pessoas que procuram um exemplo de codificação limpo de associações um-para-muitos / muitos-para-um entre as três classes participantes para armazenar atributos extras na relação, consulte este site:
bom exemplo de associações um-para-muitos / muitos-para-um entre as três classes participantes
Pense nas suas chaves primárias
Pense também na sua chave primária. Muitas vezes você pode usar chaves compostas para relacionamentos como este. A doutrina apoia nativamente isso. Você pode transformar suas entidades referenciadas em IDs. Verifique a documentação sobre chaves compostas aqui
fonte
Eu acho que iria com a sugestão do @ beberlei de usar métodos de proxy. O que você pode fazer para simplificar esse processo é definir duas interfaces:
Em seguida, você
Album
e seuTrack
podem implementá-los, enquanto oAlbumTrackReference
ainda pode implementar ambos, da seguinte maneira:Dessa forma, removendo sua lógica que está referenciando diretamente um
Track
ou umAlbum
e apenas substituindo-o para que ele use umTrackInterface
ouAlbumInterface
, você poderá usá-loAlbumTrackReference
em qualquer caso possível. O que você precisará é diferenciar um pouco os métodos entre as interfaces.Isso não diferencia o DQL nem a lógica do repositório, mas seus serviços apenas ignoram o fato de que você está passando um
Album
ou umAlbumTrackReference
, ou umTrack
ou umAlbumTrackReference
porque escondeu tudo por trás de uma interface :)Espero que isto ajude!
fonte
Primeiro, concordo principalmente com beberlei em suas sugestões. No entanto, você pode estar se projetando em uma armadilha. Seu domínio parece considerar o título a chave natural de uma faixa, o que provavelmente ocorre em 99% dos cenários que você encontra. No entanto, e se o Battery on Master of the Puppets for uma versão diferente (duração diferente, ao vivo, acústica, remix, remasterizada etc.) que a versão do The Metallica Collection .
Dependendo de como você deseja lidar (ou ignorar) esse caso, você pode seguir a rota sugerida de beberlei ou apenas seguir sua lógica extra proposta em Album :: getTracklist (). Pessoalmente, acho que a lógica extra é justificada para manter sua API limpa, mas ambas têm seu mérito.
Se você deseja acomodar meu caso de uso, o Tracks pode conter um OneToMany auto-referente a outros Tracks, possivelmente $ similarTracks. Nesse caso, haveria duas entidades para a faixa Battery , uma para a coleção Metallica e outra para o Master of the Puppets . Então, cada entidade Track similar conteria uma referência uma à outra. Além disso, isso eliminaria a classe AlbumTrackReference atual e eliminaria o seu "problema" atual. Concordo que ele está apenas movendo a complexidade para um ponto diferente, mas é capaz de lidar com um caso de uso que não era capaz anteriormente.
fonte
Você pede o "melhor caminho", mas não há o melhor. Existem muitas maneiras e você já descobriu algumas delas. Como você deseja gerenciar e / ou encapsular o gerenciamento de associação ao usar classes de associação depende inteiramente de você e do seu domínio concreto, ninguém pode mostrar o "melhor caminho", receio.
Além disso, a questão poderia ser muito simplificada, removendo o Doctrine e os bancos de dados relacionais da equação. A essência da sua pergunta se resume a uma pergunta sobre como lidar com classes de associação na OOP simples.
fonte
Eu estava obtendo um conflito com a tabela de junção definida em uma anotação de classe de associação (com campos personalizados adicionais) e uma tabela de junção definida em uma anotação de muitos para muitos.
As definições de mapeamento em duas entidades com um relacionamento direto de muitos para muitos pareciam resultar na criação automática da tabela de junção usando a anotação 'joinTable'. No entanto, a tabela de junção já foi definida por uma anotação em sua classe de entidade subjacente e eu queria que ela usasse as próprias definições de campo dessa classe de entidade de associação para estender a tabela de junção com campos personalizados adicionais.
A explicação e solução é aquela identificada pelo FMaz008 acima. Na minha situação, foi graças a este post no fórum ' Doctrine Annotation Question '. Este post chama a atenção para a documentação do Doutrine sobre relacionamentos unidirecionais ManyToMany . Observe a observação sobre a abordagem do uso de uma 'classe de entidade de associação', substituindo, assim, o mapeamento de anotação muitos-para-muitos diretamente entre duas classes principais de entidades por uma anotação um-para-muitos nas principais classes de entidade e dois 'muitos-para-muitos' -um 'anotações na classe de entidade associativa. Há um exemplo fornecido nesta postagem no fórum Modelos de associação com campos extras :
fonte
Este exemplo realmente útil. Falta na doutrina da documentação 2.
Muito obrigado.
Para os proxies, funções podem ser feitas:
e
fonte
Você está se referindo a metadados, dados sobre dados. Eu tive esse mesmo problema no projeto em que estou trabalhando no momento e tive que gastar algum tempo tentando descobrir isso. É muita informação para postar aqui, mas abaixo estão dois links que você pode achar úteis. Eles fazem referência à estrutura do Symfony, mas são baseados no 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/
Boa sorte e boas referências ao Metallica!
fonte
A solução está na documentação do Doctrine. Nas perguntas frequentes, você pode ver isso:
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
E o tutorial está aqui:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
Portanto, você não faz mais um,
manyToMany
mas precisa criar uma Entidade extra e colocarmanyToOne
em suas duas entidades.ADICIONAR para @ f00bar comentário:
é simples, você só precisa fazer algo assim:
Então você cria uma entidade ArticleTag
Espero que ajude
fonte
:(
Alguém poderia compartilhar um exemplo do terceiro caso de uso usando o formato yml? Eu realmente appriace:#
Unidirecional. Basta adicionar o inversedBy: (nome da coluna estrangeira) para torná-lo bidirecional.
Espero que ajude. Até logo.
fonte
Você pode conseguir o que deseja com a Herança de tabela de classe, onde altera AlbumTrackReference para AlbumTrack:
E
getTrackList()
conteriaAlbumTrack
objetos que você poderia usar como quiser:Você precisará examinar isso completamente para garantir que não sofra desempenho.
Sua configuração atual é simples, eficiente e fácil de entender, mesmo que algumas das semânticas não pareçam bem com você.
fonte
Ao obter todas as faixas do álbum dentro da classe, você gerará mais uma consulta para mais um registro. Isso é por causa do método proxy. Há outro exemplo do meu código (consulte a última postagem no tópico): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Existe algum outro método para resolver isso? Uma única junção não é uma solução melhor?
fonte
Aqui está a solução, conforme descrito na Documentação do Doctrine2
fonte