VB .NET 2008 - LINQ To SQL - Criando Mestre-Detalhe


Vamos dar mais um passo no nosso aprendizado do LINQ apresentando neste artigo como criar relacionamentos um-para-muitos ou modelo mestre-detalhe usando a sintaxe LINQ.

Se você esta tendo o seu primeiro contato com o LINQ veja os artigos publicados:

Requisitos necessários para acompanhar este artigo:

Abra o VB 2008 Express e crie um novo projeto no menu FIle->New Project do tipo Windows Forms Application com o nome LINQMestreDetalhes (ou algo mais sugestivo);

A seguir vamos definir uma conexão com o banco de dados Northwind.mdf, se a conexão ainda não existir no seu ambiente faça o seguinte:

1- No menu View e selecione DataBase Explorer e clique com o botão direito do  mouse sobre o item Data Connections e selecione Add Connection...

2- Na janela Add Connection Clique no botão Browse e selecione o banco de dados Northwind.mdf a partir do local onde você o instalou e clique no botão Abrir;

Agora vamos incluir a referência ao LINQ To SQL através do template LINQ To SQL Classes;

No menu Project opção Add New Item selecione na janela Templates o template LINQ to SQL Classes alterando o nome para Northwind.dbml pois iremos efetua a conexão com o banco de dados Northwind.mdf (você pode usar qualquer nome);

Neste momento será exibida a janela do Descritor Objeto Relacional e você deverá ter a seguinte visão do seu projeto no VB 2008;

Expanda os objetos do banco de dados Northwind.mdf e selecione a tabela Orders e a tabela Customers arrastando e soltando na janela Object Relational Designer;

Quando você arrasta e solta as tabelas Orders e Customers a partir da janela DataBase Explorer no descritor LINQ To SQL, o VB 2008 irá inspecionar os relacionamentos chave primária/chave estrangeira dos objetos e baseado neles irá criar associações de relacionamentos padrão entre as diferentes entidades de classes criadas.

No exemplo existe uma relação um-para-muitos entre a tabela Customers e Orders onde a vinculação é feita pela chave CustomerID onde um cliente pode possuir muitos pedidos. Neste caso Customers é a tabela Pai e Orders a tabela Filha.

No Descritor OR vemos que foram criadas duas classes - Customer e Product - onde as propriedades definidas estão mapeadas para as colunas de suas respectivas tabelas.

Vamos voltar ao formulário form1.vb e a partir da janela Data selecionar a opção Add New Data Source;

Na janela do assistente selecione a opção Object pois iremos nos conectar com o modelo criado pelo LINQ, Clique em Next>

Na próxima janela expanda o modelo de objetos criado e selecione o objeto Customer. Clique em Next> e depois em Finish.

Após isto você deverá ver na janela Data Sources as fontes de dados Customers e Orders;

Selecione Customers e arraste e solte a fonte de dados no formulário form1.vb;

Você verá a criação de um DataGridView e dos objetos CustomerBindingSource e CustomerBindingNavigator;

Em seguida ajuste o leiaute do formulário e a partir da janela Data Sources selecione, arraste e solte a fonte Orders para o formulário:

Será criado o DataGridView dos pedidos (Orders) e do objeto OrdersBindingSource;

Com isso estamos com o ambiente preparado para que havendo uma seleção de um Customer os seus pedidos (Orders) serão exibidos.

Agora temos criar o código para preencher os objetos. A primeira coisa a fazer é criar uma referência a objeto DataContext:

Private db As New NorthwindDataContext

O DataContext é o gerenciador do mapeamento objeto relacional efetuado pelo LINQ entre as classes criadas e as tabelas do banco de dados que permite a realização de consultas e atualizações no banco de dados via sintaxe LINQ.

Agora no evento Load do formulário form1.vb inclua o seguinte código:

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
     Me.CustomerBindingSource.DataSource = db.Customers
End Sub

Rode o projeto e veja o resultado:

Isso mesmo !!! Os clientes são carregados no primeiro Grid e os pedidos no segundo e , se você selecionar um determinado cliente irá perceber que o seus pedidos são exibidos no segundo grid. Tudo isso com duas linhas de código. (E, uma pequena ajuda do LINQ...)

Para vermos como as coisas funcionam vamos incluir a linha de código : db.Log = Console.Out no evento Load e rodar novamente o projeto observando a saída na janela Output;

Observe que são exibidas duas instruções SQL , a primeira referente a carga dos clientes no primeiro Grid e a segunda referente aos pedidos de um determinado cliente. Neste caso o parâmetro usado é o código do cliente (CustomerID) que é chave de relacionamento entre as tabelas.(Veja o parâmetro ALFKI no exemplo). Desta forma cada seleção no primeiro grid irá gerar um novo Select com o customerID selecionado. Isso é conhecido pelo nome de lazy loading.

Nota: Lazy loading, também conhecida como dynamic function loading, é um modo que permite o desenvolvedor especificar quais componentes de uma programação não serão carregados no armazenamento por padrão quanto o programa começar a rodar. O LINQ To SQL pode fazer determinado atributo ser carregado utilizando Lazy Loading / Delay, basta você selecionar a propriedade da classe e definir a propriedade "Delay Loaded" para True.  Isso quer dizer que só são trazidos os dados que são utilizados, ou seja, são obtidos por demanda. Isso implica em melhoria de performance e menos consumo de memória.

Vamos agora tratar de permitir que os dados sejam persistidos no banco de dados . No formulário form1.vb selecione o ícone Save e clique com o botão direito do mouse sobre ele escolhendo a opção Enabled no menu suspenso.

A seguir clique duas vezes sobre o ícone habilitado e no evento Click o código que irá salvar os objetos no banco de dados:

Private Sub CustomerBindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CustomerBindingNavigatorSaveItem.Click

Me.Validate()
Me.CustomerBindingSource.EndEdit()
Me.OrdersBindingSource.EndEdit()

Try
     db.SubmitChanges()
     MsgBox("Salvo")
Catch ex As Exception
     MsgBox("Erro ao salvar : " & ex.Message)
End Try

End Sub

O método SubmitChanges do objeto DataContext define o conjunto de objetos modificados que serão incluídos, atualizados ou excluídos e executa o comando apropriado para implementar as alterações no banco de dados.

A seguir rode o projeto e clique no botão para incluir um novo cliente/pedido e altere também os dados de um cliente clicando a seguir no botão Save;

Perceba que inclui um novo cliente e seu pedido e alterei o campo ContactTitle da tabela Customers. Clicando no botão Save as alterações são salvas no banco de dados.

Agora veja abaixo a instrução SQL gerada pelo LINQ:

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [dbo].[Customers] AS [t0]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [WOLZA]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

INSERT INTO [dbo].[Customers]([CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)
-- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0) [MACOR]
-- @p1: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [JcmSoft]
-- @p2: Input NVarChar (Size = 9; Prec = 0; Scale = 0) [Macoratti]
-- @p3: Input NVarChar (Size = 4; Prec = 0; Scale = 0) [Ower]
-- @p4: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p5: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p6: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p7: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p8: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p9: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p10: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

INSERT INTO [dbo].[Orders]([CustomerID], [EmployeeID], [OrderDate], [RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName], [ShipAddress], [ShipCity], [ShipRegion], [ShipPostalCode], [ShipCountry])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12)

SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]
-- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0)
[MACOR]
-- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [11/10/2006 00:00:00]
-- @p3: Input DateTime (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p4: Input DateTime (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p5: Input Int (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p6: Input Money (Size = 0; Prec = 19; Scale = 4) [Null]
-- @p7: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p8: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p9: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p10: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p11: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p12: Input NVarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

UPDATE [dbo].[Customers]
SET [ContactTitle] = @p10
WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] IS NULL) AND ([PostalCode] = @p6) AND ([Country] = @p7) AND ([Phone] = @p8) AND ([Fax] = @p9)
-- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0)
[WOLZA]
-- @p1: Input NVarChar (Size = 14; Prec = 0; Scale = 0) [Wolski Zajazd]
-- @p2: Input NVarChar (Size = 23; Prec = 0; Scale = 0) [Zbyszek Piestrzeniewicz]
-- @p3: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [Owner]
-- @p4: Input NVarChar (Size = 15; Prec = 0; Scale = 0) [ul. Filtrowa 68]
-- @p5: Input NVarChar (Size = 8; Prec = 0; Scale = 0) [Warszawa]
-- @p6: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [01-012]
-- @p7: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Poland]
-- @p8: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [(26) 642-7012]
-- @p9: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [(26) 642-7012]
-- @p10: Input NVarChar (Size = 8; Prec = 0; Scale = 0) [OwerXXXX]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

Note o Select inicial e a seguir o INSERT INTO para incluir o novo registro e o comando UPDATE para atualizar o campo alterado.

Vamos agora incrementar um pouco a nossa aplicação permitindo filtrar os clientes para um determinado país selecionado.

No formulário form1.vb selecione o CustomerBindingNavigator e inclua um combobox, clicando sobre o controle e selecionando ComboBox:

Vamos preencher o combobox com os paises. Para isso no evento Load , comente o código existente e inclua o seguinte código:

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

db.Log = Console.Out

'Me.CustomerBindingSource.DataSource = db.Customers

Dim paises = From clientes In db.Customers _
                     Where clientes.Country <> "" _
                     Order By clientes.Country _
                     Select clientes.Country Distinct

For Each pais In paises
      Me.ToolStripComboBox1.Items.Add(pais)
Next

End Sub

Feito isso temos que refletir a seleção de um pais no DataGridView para que sejam exibidos os clientes para o país selecionado no ComboBox.

Então no evento SelectedIndexChanged do ToolStripCombobox vamos incluir o código que filtra os clientes por pais usando LINQ To SQL;

Private Sub ToolStripComboBox1_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ToolStripComboBox1.SelectedIndexChanged

Dim clientes = From cliente In db.Customers _
                      Where cliente.Country = Me.ToolStripComboBox1.SelectedItem.ToString _
                       Select cliente

Me.CustomerBindingSource.DataSource = clientes

End Sub

Executando o projeto o formulário será apresentando vazio , onde teremos que selecionar um país a partir da combobox. Feito isso os clientes e os pedidos dos clientes para o pais selecionado é exibido conforme figuras abaixo:

É sempre uma boa idéia limitar a quantidade de dados exibida na carga de um formulário, com isso temos desempenho e evitamos problemas.

Espero que ao final deste artigo os seus conhecimentos sobre LINQ To SQL estejam mais sólidos

Pegue o projeto completo aqui: LINQToSQL_MestreDetalhes.zip

Eu sei é apenas .NET mas eu gosto...

Referências:

LINQ - http://msdn2.microsoft.com/en-us/netframework/aa904594.aspx
LINQ To SQL - http://msdn2.microsoft.com/en-us/library/bb425822.aspx

ScottGu´s Blog - http://weblogs.asp.net/scottgu/archive/2006/09/01/Understanding-LINQ-to-SQL-Query-Translations.aspx
Exemplos- 101 LINQ Samples


José Carlos Macoratti