sexta-feira, 13 de fevereiro de 2015

Entendendo os JOINs no Entity Framework

Olá, pessoal!

Para quem leu os meus posts anteriores, estou agora me aprofundando na plataforma .NET e gostaria de compartilhar um pouco do que aprendi. E, como seria possível imaginar para quem veio do mundo Java com Hibernate, logo minha curiosidade voltou-se ao principal framework de persistência da plataforma. Por esta razão, venho hoje para falar um pouco do Entity Framework.

Uma dificuldade inicial que tive foi de entender como funcionavam os JOINs no Entity Framework. Não pela complexidade no entendimento em si, mas pela falta de um texto que sintetizasse bem as dúvidas de como o Entity Framework gerava os SQLs nas consultas montadas com LINQ e expressões Lambda. Como desenvolvedor, é importante eu saber como e quando o framework faz um LEFT JOIN, quando quero especificar mais de uma condição em um JOIN, como posso retornar apenas alguns valores, etc.

O JOIN, LEFT JOIN e Include


Como exemplo, vamos considerar o relacionamento entre 2 entidades: Pessoa e Endereço. Neste exemplo, uma Pessoa precisa ter um Endereço. O mapeamento seria:

modelBuilder.Entity<Pessoa>()
    .HasRequired(x => x.Endereco)
    .WithMany()
    .HasForeignKey(x => x.EnderecoID);

Podemos observar no mapeamento que especifiquei o relacionamento como HasRequired. Assim, estou dizendo ao Entity Framework que toda Pessoa deve ter, obrigatoriamente, Endereço. Ou seja, se eu fizer uma busca como esta:

var pessoas = context.Pessoas
.Include(p => p.Endereco)
.ToList();

Terei como resultado uma lista de pessoas com o endereço de cada uma 1.

Quando utilizo o "Include", o Entity Framework faz o JOIN no SQL entre Pessoa e Endereço. A consulta SQL gerada seria algo parecido com esta:

select p.*, e.* from Pessoa p
JOIN Endereco e ON p.EnderecoID = e.ID

Mas, digamos que eu não quero um JOIN, pois o Endereço na Pessoa é opcional. Há pessoas que não tem endereço e eu gostaria de trazê-las também no resultado da consulta. Se é opcional, então não usamos HasRequired, e sim HasOptional no mapeamento:

modelBuilder.Entity<Pessoa>()
    .HasOptional(x => x.Endereco)
    .WithMany()
    .HasForeignKey(x => x.EnderecoID);

Com este mapeamento, a mesma consulta anterior vai fazer um LEFT JOIN entre Pessoa e Endereço:

select p.*, e.* from Pessoa p
LEFT JOIN Endereco e ON p.EnderecoID = e.ID

Ou seja, quando fazemos o famoso "Include", o Entity Framework vai decidir se vai usar o JOIN ou LEFT JOIN olhando se o mapeamento entre as entidades está HasRequired ou HasOptional, respectivamente.

No próximo texto vou falar de como montar uma consulta com LINQ e expressões Lambda para fazer um JOIN ou LEFT JOIN caso precise "desrespeitar" o mapeamento vigente entre e entidades, mostrar como fazer um JOIN/LEFT JOIN com mais de uma condição, eager loading, lazy loading entre outras coisas. Até mais!

Bônus: use o WithMany com sabedoria


Outro comentário importante, que vai mais como um bônus, é no uso do WithMany. Já observei que muitas pessoas sempre configuram o WithMany sem pensar na razão de estarem fazendo isto, pois não é obrigatório no mapeamento. O WithMany nada mais é do que o mapeamento do "outro lado" do relacionamento. Ou seja, ainda usando nosso exemplo entre Pessoa e Endereço, poderíamos ter:

modelBuilder.Entity<Pessoa>()
    .HasOptional(x => x.Endereco)
    .WithMany(y => y.Pessoas)
    .HasForeignKey(x => x.EnderecoID);

No mapeamento acima, a entidade de Endereço tem uma lista de pessoas. Mas, digamos que para o nosso sistema não faz sentido saber quais pessoas pertencem a determinado Endereço. Neste caso, qual a vantagem de fazer o relacionamento dos 2 lados?

Nenhuma. Na verdade, há desvantagens como: poluir o código, aumentar a complexidade e até mesmo criar problemas de performance.

Sim, problemas de performance. Não pelo mapeamento em si, mas pelo risco de isto acontecer por uso indevido. Explico.

Digamos que você está em um sistema bancário que tem a entidade Movimentacao e ContaCorrente. Faz perfeitamente sentido a Movimentacao pertencer a uma ContaCorrente e ter este relacionamento mapeado no sistema. Porém, é muito perigoso o inverso: a ContaCorrente ter uma lista mapeada de Movimentacao, afinal, pode haver milhares de movimentações! Deixar algo assim facilmente "acessível" no mapeamento, podendo ser carregado na memória a qualquer instante, pode gerar sérios problemas na aplicação.

1. Para quem vem do mundo Hibernate, esta consulta mostrada tem um comportamento igual ao de um FETCH JOIN.