LEFT JOIN com diferentes classes no Linq
Autor: Gabriel Bauermann | Publicado: 2012-08-21T22:31:00-03:00


O artigo a seguir é mais uma colaboração de Samir Lohmann. Desta vez Samir aborda a utilização de Left Join no Linq.






O Linq facilita bastante a combinação de dados de diferentes
fontes, tais como DataSets, listas, etc., com o uso de JOINS. 



Entretanto, quem começa a usar esse recurso, cedo ou tarde,
vai querer criar um LEFT JOIN.
Embora isso seja possível, não é muito óbvio. Como veremos, o Linq sequer
define a palavra LEFT



Para começar, vamos criar um exemplo para estabelecer o
conceito de um LEFT JOIN:



Digamos que eu tenha uma lista com quatro pessoas. Essas
pessoas estão matriculadas em diferentes matérias de um curso universitário de
Sistemas de In formação, e possuem notas em cada uma dessas matérias. 



Eu quero obter uma lista das pessoas com suas respectivas
matérias e notas; mas, se uma dessas pessoas NÃO estiver matriculada, eu quero
exibir o nome dela também, com a informação de que ela não está matriculada.



Lista de pessoas (tabela da esquerda):



 

  
  
ID

  
  
Nome

  
  
Idade

 

  
  
1

  
  
João

  
  
20

 

  
  
2

  
  
Maria

  
  
21

 

  
  
3

  
  
José

  
  
22

 

  
  
4

  
  
Ana

  
  
23

 




Lista de matérias com suas notas (tabela da direita):



 

  
  
ID Pessoa

  
  
Matéria

  
  
Nota

 

  
  
1

  
  
Bancos de Dados

  
  
9

 

  
  
2

  
  
Programação 1

  
  
8.5

 

  
  
3

  
  
Antropologia

  
  
5

 

  
  
1

  
  
Programação 1

  
  
8.8

 




Notem que a Ana (pessoa com ID = 4) não possui nenhum
registro de matéria e nota.



Vamos ao código: primeiro, defino uma classe Pessoa.



''' &lt;summary&gt;

''' Representa uma pessoa com seus dados

''' &lt;/summary&gt;

''' &lt;remarks&gt;&lt;/remarks&gt;



Public Class Pessoa



&nbsp;&nbsp;&nbsp; Public ID As String

&nbsp;&nbsp;&nbsp; Public Nome As String

&nbsp;&nbsp;&nbsp; Public Idade As Integer



&nbsp;&nbsp;&nbsp; Public Sub New(ByVal pID As String, ByVal pNome As String, ByVal pIdade As Integer)

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ID = pID

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Nome = pNome

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Idade = pIdade

&nbsp;&nbsp;&nbsp; End Sub



End Class



Eu usarei um DataTable para armazenar os dados de notas,
então não vou declarar classe para isto. 



Nota:

Eu poderia criar uma classe para
isto também, mas um dos meus objetivos é mostrar um JOIN com objetos de classes
diferentes. Isso muitas vezes acontece na prática, por exemplo, quando obtenho
os dados de sistemas diferentes.



O próximo passo é popular uma lista de pessoas e o DataTable
que acabei de comentar:



Dim dtNotas As New DataTable



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' - criação
de dados fictícios para o exemplo
----------------------------------------------------



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Dim
lstPessoas As New
List(Of Pessoa)

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lstPessoas.Add(New
Pessoa("1", "João", 20))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lstPessoas.Add(New
Pessoa("2", "Maria", 21))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lstPessoas.Add(New
Pessoa("3", "José", 22))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lstPessoas.Add(New
Pessoa("4", "Ana", 23))



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' --



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
dtNotas.Columns.Add("ID Pessoa",
Type.GetType("System.String"))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
dtNotas.Columns.Add("Matéria",
Type.GetType("System.String"))

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
dtNotas.Columns.Add("Nota",
Type.GetType("System.Decimal"))



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Dim r As DataRow



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r = dtNotas.NewRow()



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("ID
Pessoa") = "1"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Matéria")
= "Bancos de Dados"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Nota")
= 9



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dtNotas.Rows.Add(r)



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' --



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r = dtNotas.NewRow()



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("ID
Pessoa") = "2"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Matéria")
= "Programação 1"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Nota")
= 8.5



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dtNotas.Rows.Add(r)



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' --



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r = dtNotas.NewRow()



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("ID
Pessoa") = "3"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Matéria")
= "Antropologia"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Nota")
= 5



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dtNotas.Rows.Add(r)



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' --



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r = dtNotas.NewRow()



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("ID
Pessoa") = "1"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Matéria")
= "Programação 1"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r("Nota")
= 8.8



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dtNotas.Rows.Add(r)



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; '
---------------------------------------------------------------------------------------------------------------------------



Agora começa o Linq.



Em primeiro lugar, quem conhece SQL sabe que, se uma linha
da tabela da esquerda no LEFT
JOIN não tiver uma ou mais correspondentes na tabela da direita, o SGBD
vai devolver uma linha com os valores da tabela da esquerda mais NULLs para
todos os valores da linha da direita.



No Linq, faremos um
pouco diferente. Ao invés de NULLs, vamos especificar um objeto default para
ser devolvido quando não tivermos nada no lado direito.



No exemplo, a tabela
da direita é um DataTable que, para o Linq, será interpretado como um IEnumerable
(Of DataRow) . Ou seja, o item default a ser criado é um DataRow, mas poderia
ser de qualquer outro tipo:



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' Este é o
registro de matéria + nota default, que será usando quando não houver nenhum

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ' para uma determinada pessoa



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Dim
rDefault As DataRow

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rDefault = dtNotas.NewRow()

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rDefault("ID Pessoa") = ""

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rDefault("Matéria") = "[Sem
Registro]"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rDefault("Nota") = -1



Por fim, a consulta Linq propriamente dita:



1 &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Dim q = From p In lstPessoas
_

2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Group Join n In dtNotas.AsEnumerable _

3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
On p.ID Equals
n("ID Pessoa") _

4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Into grp = Group _

5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; From it In
grp.DefaultIfEmpty(rDefault) _

6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Select Nome = p.Nome, Materia = it("Matéria"), Nota = it("Nota")



Nas linhas 1 a 4, estou fazendo o JOIN. Em um primeiro
momento, a tabela da direita vai entrar em um grupo, chamado grp.



Na linha 5, estou buscando os itens desse grupo que ficarão
disponíveis no select. Note que é aqui que estou dizendo que itens inexistentes
implicarão na criação de um default, através do m



Por fim, na linha 6, faço o select.



Façamos um loop nos resultados da consulta:



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; For Each it In q.ToList



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Debug.Print(String.Format("Nome: {0} | Matéria: {1} | Nota: {2} {3}",
it.Nome, it.Materia, it.Nota, vbCrLf))



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Next



Resultado:



Nome: João | Matéria: Bancos de Dados | Nota: 9

Nome: João | Matéria: Programação 1 | Nota: 8,8

Nome: Maria | Matéria: Programação 1 | Nota: 8,5

Nome: José | Matéria: Antropologia | Nota: 5

Nome: Ana | Matéria: [Sem Registro] | Nota: -1



Uma dúvida que provavelmente vai surgir é:



Preciso mesmo desse
grupo(grp)? Qual é a utilidade dele?



Resposta: sim, precisa, pois o Linq só consegue fazer um
LEFT JOIN quando a tabela da direita é um grupo (para quem não entende esse
conceito de grupo, veja meu artigo anterior Linq... qual é a utilidade mesmo?).



Para provar, podemos testar a consulta abaixo, que compila,
mas NÃO funciona como um LEFT JOIN.



&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Dim q2 = From p In lstPessoas
_

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Join n In
dtNotas.AsEnumerable.DefaultIfEmpty(rDefault) _

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
On p.ID Equals
n("ID Pessoa") _

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Select Nome = p.Nome,
Materia = n("Matéria"), Nota = n("Nota")



Tentei pesquisar o motivo
disto, mas não achei nenhuma boa justificativa, já que a sintaxe acima seria
bem mais limpa. Provavelmente, os projetistas do Linq devem ter tido um bom
motivo.



Abraços a todos, em especial ao Samir, por mais esta colaboração.