
Doctrine também suporta o relacionamento N:M ou muitos para muitos como no caso da figura ao lado, um ator pode fazer vários filmes e um filme pode ter vários atores. Essa definição é um pouco diferente das demais, por exemplo um relacionamento entre de usuários e grupos, ambas as entidades tem que receber uma definição para a relação e uma nova tabela é criada para ligar as duas relações.
No modelo do sakila, existe um N:M entre filme e ator através da tabela film_actor, ao gerar os modelos esse relacionamento foi identificado da seguinte maneira.
// models/generated/BaseActor.php ... public function setUp() { parent::setUp(); $this->hasMany('FilmActor', array( 'local' => 'actor_id', 'foreign' => 'actor_id')); } ... // models/generated/BaseFilm.php public function setUp() { parent::setUp(); ... $this->hasMany('FilmActor', array( 'local' => 'film_id', 'foreign' => 'film_id')); ... } // models/generated/BaseFilmActor.php ... public function setUp() { parent::setUp(); $this->hasOne('Actor', array( 'local' => 'actor_id', 'foreign' => 'actor_id')); $this->hasOne('Film', array( 'local' => 'film_id', 'foreign' => 'film_id')); } ...
Então para listar todos os atores de um filme você pode o seguinte:
<?php // many2many.php require 'bootstrap'; $filme = Doctrine::getTable('Film')->find(231); printf("Titulo: %s \nDescrição: %s \nAtores:", $filme->title, $filme->description); foreach($filme->FilmActor as $fa) printf(" %s %s \n", $fa->Actor->first_name, $fa->Actor->last_name);
E terá uma lista como essa:
$ php many2many.php Titulo: DINOSAUR SECRETARY Descrição: A Action-Packed Drama of a Feminist And a Girl who must Reach a Robot in The Canadian Rockies Atores: LUCILLE TRACY BURT DUKAKIS JAYNE NEESON RUSSELL BACALL PENELOPE MONROE MINNIE KILMER
A definição esta correta e funciona mas como o carregamento de atores é “preguiçoso” a cada iteração na coleção $filme->FilmActor ao acessar a sua propriedade Actor estaremos executando uma nova query. Teremos 1 query na tabela Film, 1 na tabela FilmActor e mais 6 na Actor para buscar cada um dos atores.
Como o Doctrine não advinha qual sua intenção nesse esquema você pode definir uma nova relação para que ele busque os dados de forma mais eficiente. Para criar uma nova relação vamos usar a classe Film e sobrescrever o método setUp.
<?php /** * Film * * This class has been auto-generated by the Doctrine ORM Framework * * @package ##PACKAGE## * @subpackage ##SUBPACKAGE## * @author ##NAME## <##EMAIL##> * @version SVN: $Id: Builder.php 6401 2009-09-24 16:12:04Z guilhermeblanco $ */ class Film extends BaseFilm { public function setUp() { // Não esqueça de invocar o setUp da classe pai para // não perder os relacionamentos já definidos parent::setUp(); $this->hasMany('Actor as Atores', array( 'local' => 'film_id', 'foreign' => 'actor_id', 'refClass' => 'FilmActor')); } }
A relação criada liga diretamente as entidades Film e Actor com as respectivas chave primaria e extrangeira e adiciona a nova opção refClass. Essa opção cria uma referência para a tabela FilmActor assim você não precisa mais acessar os atores através da propriedade FilmActor e sim do alias “Atores”.
<?php // many2many.php require 'bootstrap'; $filme = Doctrine::getTable('Film')->find(231); printf("Titulo: %s \nDescrição: %s \nAtores:", $filme->title, $filme->description); foreach($filme->Atores as $a) printf(" %s %s \n", $a->first_name, $a->last_name);
O resultado é o mesmo mas além da sintaxe ter ficado mais curta e sua intenção explicita, apenas 2 querys serão executadas. Uma na tabela film e outra para buscar todos os atores relacionados, está é a query escrita pelo Doctrine:
SELECT a.actor_id AS a__actor_id, a.first_name AS a__first_name, a.last_name AS a__last_name, a.last_update AS a__last_update, f.actor_id AS f__actor_id, f.film_id AS f__film_id, f.last_update AS f__last_update FROM actor a LEFT JOIN film_actor f ON a.actor_id = f.actor_id WHERE f.film_id IN (231)
A nova definição foi usada e o Doctrine pode “entender” como buscar os dados sem ter que executar várias querys. No outro lado do relacionamento você pode fazer uma nova definição assim:
<?php /** * Actor * * This class has been auto-generated by the Doctrine ORM Framework * * @package ##PACKAGE## * @subpackage ##SUBPACKAGE## * @author ##NAME## <##EMAIL##> * @version SVN: $Id: Builder.php 6401 2009-09-24 16:12:04Z guilhermeblanco $ */ class Actor extends BaseActor { public function setUp() { parent::setUp(); $this->hasMany('Film as Filmes', array( 'local' => 'actor_id', 'foreign' => 'film_id', 'refClass' => 'FilmActor')); } }
Agora você pode acessar normalmente as propriedades de ator e terá uma nova com o nome do alias “Filmes” contendo uma coleção de filmes feitos pelo ator. Para listar estes filmes é simples:
$ator = Doctrine::getTable('Actor')->find(65); printf("Atriz: %s %s\n", $ator->first_name, $ator->last_name); print "Filmes:"; foreach($ator->Filmes as $f) printf(" %s \n", $f->title);
Atriz: ANGELA HUDSON Filmes: ARMAGEDDON LOST AUTUMN CROW BRIDE INTRIGUE BULWORTH COMMANDMENTS ...
Essas relações também podem ser usadas em querys escritas com a DQL. Supondo que queira listar os filmes feitos por uma determinada atriz mas queira limitar os filmes com restrições a menores de 13 anos:
$ator = Doctrine_Query::create() ->from('Actor a') ->leftJoin('a.Filmes f') ->where('a.actor_id = ? AND f.rating = ?') ->execute(array(65, 'PG-13')); printf("Atriz\n %s %s\nFilmes\n", $ator[0]->first_name, $ator[0]->last_name); foreach ($ator[0]->Filmes as $f) print ' ' . $f->title . "\n";
Note que o novo relacionamento definido também foi acessado através de seu alias em leftJoin(‘a.Filmes f’). O resultado do script:
Atriz ANGELA HUDSON Filmes GAMES BOWFINGER KICK SAVANNAH MALTESE HOPE METAL ARMAGEDDON NASH CHOCOLAT PARIS WEEKEND PREJUDICE OLEANDER ROBBERS JOON VOYAGE LEGALLY
Com a DQL acima o Doctrine irá escrever a seguinte query em SQL:
SELECT a.actor_id AS a__actor_id, a.first_name AS a__first_name, a.last_name AS a__last_name, a.last_update AS a__last_update, f.film_id AS f__film_id, f.title AS f__title, f.description AS f__description, f.release_year AS f__release_year, f.language_id AS f__language_id, f.original_language_id AS f__original_language_id, f.rental_duration AS f__rental_duration, f.rental_rate AS f__rental_rate, f.length AS f__length, f.replacement_cost AS f__replacement_cost, f.rating AS f__rating, f.special_features AS f__special_features, f.last_update AS f__last_update FROM actor a LEFT JOIN film_actor f2 ON (a.actor_id = f2.actor_id) LEFT JOIN film f ON f.film_id = f2.film_id WHERE (a.actor_id = 65 AND f.rating = 'PG-13')
Pode parecer um pouco trabalhoso ter que definir essas relações manualmente mas como pode ver isso pode ser bem util. Uma query grande como a do exemplo acima foi escrita de maneira bem simples e compacta em DQL, interpretada corretamente pelo ORM e tem abstração de banco de dados.
Esse também foi muito bom.
Parabéns..
Cara, queria saber quais as outras formas de buscar os dados, pq com o find eu preciso digitar um id, e com o findAll ele me traz todos, não tem uma forma mais simples, sem usar as DQL?
Obrigado.
Da uma olhada nesse post em “retrieve”, comentei sobre as outras formas alem do find para busca.
Muito bom cara. Tava procurando alguma referencia clara explicando como fazer essa jogada do refClass. Tava precisando disso pra deixar mais claro os meus relacionamentos, ja q eu tava caindo em um redemoinho de nomes de tabelas auto-explicativas, quando 1 palavra resolveria, so não sabia como trocar o Alias do relacionamento.
Valew mesmo, seu blog ta no meus favoritos, continua escrevendo.