;

terça-feira, 21 de agosto de 2012

LEFT JOIN com diferentes classes no Linq

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.

''' <summary>
''' Representa uma pessoa com seus dados
''' </summary>
''' <remarks></remarks>

Public Class Pessoa

    Public ID As String
    Public Nome As String
    Public Idade As Integer

    Public Sub New(ByVal pID As String, ByVal pNome As String, ByVal pIdade As Integer)
        ID = pID
        Nome = pNome
        Idade = pIdade
    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

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

        Dim lstPessoas As New List(Of Pessoa)
        lstPessoas.Add(New Pessoa("1", "João", 20))
        lstPessoas.Add(New Pessoa("2", "Maria", 21))
        lstPessoas.Add(New Pessoa("3", "José", 22))
        lstPessoas.Add(New Pessoa("4", "Ana", 23))

        ' --

        dtNotas.Columns.Add("ID Pessoa", Type.GetType("System.String"))
        dtNotas.Columns.Add("Matéria", Type.GetType("System.String"))
        dtNotas.Columns.Add("Nota", Type.GetType("System.Decimal"))

        Dim r As DataRow

        r = dtNotas.NewRow()

        r("ID Pessoa") = "1"
        r("Matéria") = "Bancos de Dados"
        r("Nota") = 9

        dtNotas.Rows.Add(r)

        ' --

        r = dtNotas.NewRow()

        r("ID Pessoa") = "2"
        r("Matéria") = "Programação 1"
        r("Nota") = 8.5

        dtNotas.Rows.Add(r)

        ' --

        r = dtNotas.NewRow()

        r("ID Pessoa") = "3"
        r("Matéria") = "Antropologia"
        r("Nota") = 5

        dtNotas.Rows.Add(r)

        ' --

        r = dtNotas.NewRow()

        r("ID Pessoa") = "1"
        r("Matéria") = "Programação 1"
        r("Nota") = 8.8

        dtNotas.Rows.Add(r)

        ' ---------------------------------------------------------------------------------------------------------------------------

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:

        ' Este é o registro de matéria + nota default, que será usando quando não houver nenhum
        ' para uma determinada pessoa

        Dim rDefault As DataRow
        rDefault = dtNotas.NewRow()
        rDefault("ID Pessoa") = ""
        rDefault("Matéria") = "[Sem Registro]"
        rDefault("Nota") = -1

Por fim, a consulta Linq propriamente dita:

1        Dim q = From p In lstPessoas _
2                Group Join n In dtNotas.AsEnumerable _
3                    On p.ID Equals n("ID Pessoa") _
4                Into grp = Group _
5                From it In grp.DefaultIfEmpty(rDefault) _
6                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:

        For Each it In q.ToList

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

        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.

        Dim q2 = From p In lstPessoas _
                 Join n In dtNotas.AsEnumerable.DefaultIfEmpty(rDefault) _
                    On p.ID Equals n("ID Pessoa") _
                 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.