Continuando a falar de LINQ to SQL, montei uma página simples, com um gridview e uma consulta com um join às tabelas SalesOrderHeaders e Contact. A idéia é exibir os pedidos e o contato colocado no pedido.
Tudo funcionou muito bem, com pouquíssimo código e sem ter que ficar me preocupando com strings de conexão, sem precisar compor código SQL, tudo na base do objeto, em suma: lindo (não fosse o problema do qual falei ontem, seria maravilhoso). É o tipo de código que todo mundo gosta de usar em palestras: parece que tudo foi feito para te dar o máximo de produtividade.
Montei um diagrama LINQ to SQL bem simples, arrastando e soltando as referidas tabelas:
Vejam o resultado abaixo, na página SemPerformance.aspx:
O código da aspx é o seguinte:
1 <%@ Page Language="vb" AutoEventWireup="false" CodeBehind="SemPerformance.aspx.vb" Inherits="LINQToSQLLoadWith.SemPerformance" %>
2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3 <html xmlns="http://www.w3.org/1999/xhtml" >
4 <head runat="server">
5 <title>Sem Performance</title>
6 </head>
7 <body>
8 <form id="form1" runat="server">
9 <div>
10 <asp:GridView ID="gvOrders" runat="server" AutoGenerateColumns="False">
11 <Columns>
12 <asp:BoundField DataField="SalesOrderID" HeaderText="Order ID" />
13 <asp:BoundField DataField="OrderDate" HeaderText="Order Date"
14 DataFormatString="{0:d}" />
15 <asp:TemplateField HeaderText="Customer">
16 <ItemTemplate>
17 <asp:Label ID="Label1" runat="server" Text='<%# Eval("Contact.FirstName") & " " & Eval("Contact.LastName") %>'></asp:Label>
18 </ItemTemplate>
19 </asp:TemplateField>
20 <asp:BoundField DataField="TotalDue" HeaderText="Total Due"
21 DataFormatString="{0:F}" />
22 </Columns>
23 </asp:GridView>
24 </div>
25 </form>
26 </body>
27 </html>
O código VB por trás da página é o seguinte:
1 Public Partial Class SemPerformance
2 Inherits System.Web.UI.Page
3
4 Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
5 If Not Page.IsPostBack Then
6 Dim orders = GetOrders(0, 10)
7 gvOrders.DataSource = orders
8 gvOrders.DataBind()
9 End If
10 End Sub
11
12 Public Function GetOrders(ByVal page As Integer, ByVal pageSize As Integer) As IEnumerable(Of SalesOrderHeader)
13
14 Dim dc As New AdventureWorksDataContext
15
16 Dim query = From so In dc.SalesOrderHeaders _
17 Take pageSize * (page + 1) Skip page * pageSize
18 Return query
19
20 End Function
21
22 End Class
Tudo certo, certo? Errado. COmo vocês viram, estou fazendo uma query no LINQ somente à tabela SalesOrerHeaders. Por causa so relacionamento entre as duas tabelas, existe uma propriedade Contact nas linhas desta tabela, que me retorna o objeto Contact referido. Notem que na linha 17 do código ASPX estou chamando esta propriedade, e solicitando as colunas FirstName e LastName. Fiquei curioso para saber como o LINQ to SQL iria lidar com isso, afinal, a chamada ao banco só acontece na última hora (chamada de recuperação preguiçosa – lazy), ou seja, somente quando chamamos DataBind. Abri o SQL Profiler para espiar, e vejam o que vi:
Um monte de solicitações ao banco… Abaixo as 3 primeiras instruções:
1 SELECT TOP (10) [t0].[SalesOrderID], [t0].[RevisionNumber], [t0].[OrderDate], [t0].[DueDate], [t0].[ShipDate], [t0].[Status], [t0].[OnlineOrderFlag], [t0].[SalesOrderNumber], [t0].[PurchaseOrderNumber], [t0].[AccountNumber], [t0].[CustomerID], [t0].[ContactID], [t0].[SalesPersonID], [t0].[TerritoryID], [t0].[BillToAddressID], [t0].[ShipToAddressID], [t0].[ShipMethodID], [t0].[CreditCardID], [t0].[CreditCardApprovalCode], [t0].[CurrencyRateID], [t0].[SubTotal], [t0].[TaxAmt], [t0].[Freight], [t0].[TotalDue], [t0].[Comment], [t0].[rowguid], [t0].[ModifiedDate]
2 FROM [Sales].[SalesOrderHeader] AS [t0]
3 go
4 exec sp_executesql N'SELECT [t0].[ContactID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailAddress], [t0].[EmailPromotion], [t0].[Phone], [t0].[PasswordHash], [t0].[PasswordSalt], [t0].[AdditionalContactInfo], [t0].[rowguid], [t0].[ModifiedDate]
5 FROM [Person].[Contact] AS [t0]
6 WHERE [t0].[ContactID] = @p0',N'@p0 int',@p0=378
7 go
8 exec sp_executesql N'SELECT [t0].[ContactID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailAddress], [t0].[EmailPromotion], [t0].[Phone], [t0].[PasswordHash], [t0].[PasswordSalt], [t0].[AdditionalContactInfo], [t0].[rowguid], [t0].[ModifiedDate]
9 FROM [Person].[Contact] AS [t0]
10 WHERE [t0].[ContactID] = @p0',N'@p0 int',@p0=216
Na verdade o "monte" de solicitações eram na verdade 11. Porque 11? porque a primeira era um select simples, feito quando eu chamei Databind contra um IEnumerable(Of SalesOrderHeader) (na verdade uma System.Data.Linq.Table(Of SalesOrderHeader)). Os outros 10 foram feitos cada vez que o binding de uma linha encontrava a expressão Contact.LastName, quando era necessário preencher esta linha de Contact. Após a chamada de LastName, a linha já estava na memória, e Contact.FirstName vinha de lá. Como cada linha passa pelo binding sozinha, foram 10 chamadas…
Como resolver isso? Se você estiver com uma latência muito alta até o servidor, essas outras 10 chamadas vão pesar. O jeito é utilizar uma propriedade do Datacontext chamada LoadOptions, do tipo System.Data.Linq.DataLoadOptions. Esse tipo possui um método com a seguinte assinatura: LoadWith(Of T)(ByVal expression As System.Linq.Expressions.Expression(Of System.Func(Of T, Object))). Ou seja, é um método que aceita uma expressão do tipo de uma lambda (ou seja, um delegate, ou seja, um "ponteiro" para uma outra função). Depois eu conto aqui o que é uma expressão, que, simplificando é um tipo novo do LINQ (e sem ele não existe LINQ como o conhecemos). Por enquanto, pense que o método aceita um outro método ao ser chamado. Note também que ele é tipado, ou seja, é um método de algum tipo. Ele serve para dizer ao datacontext que, quando carregar algum tipo (leia tabela – no nosso caso SalesOrderHeader), que deve carregar outro tipo junto (no nosso caso Contact). O código fica assim:
12 Public Function GetOrders(ByVal page As Integer, ByVal pageSize As Integer) As IEnumerable(Of SalesOrderHeader)
13
14 Dim dc As New AdventureWorksDataContext
15
16 Dim dlo As New Data.Linq.DataLoadOptions
17 dlo.LoadWith(Of SalesOrderHeader)(Function(o) o.Contact)
18 dc.LoadOptions = dlo
19
20 Dim query = From so In dc.SalesOrderHeaders _
21 Take pageSize * (page + 1) Skip page * pageSize
22 Return query
23
24 End Function
Foram adicionadas as linhas em amarelo. Assim, estou dizendo ao datacontext o seguinte: "quando você carregar o tipo SalesOrderHeader, carregue com ele (LoadWith) o tipo Contact" (linha 17). Todo o resto do código não muda.
O resultado era exatamente o que eu estava buscando. O profiler não me deixa mentir:
Abaixo o código executado:
1 SELECT TOP (10) [t0].[SalesOrderID], [t0].[RevisionNumber], [t0].[OrderDate], [t0].[DueDate], [t0].[ShipDate], [t0].[Status], [t0].[OnlineOrderFlag], [t0].[SalesOrderNumber], [t0].[PurchaseOrderNumber], [t0].[AccountNumber], [t0].[CustomerID], [t0].[ContactID], [t0].[SalesPersonID], [t0].[TerritoryID], [t0].[BillToAddressID], [t0].[ShipToAddressID], [t0].[ShipMethodID], [t0].[CreditCardID], [t0].[CreditCardApprovalCode], [t0].[CurrencyRateID], [t0].[SubTotal], [t0].[TaxAmt], [t0].[Freight], [t0].[TotalDue], [t0].[Comment], [t0].[rowguid], [t0].[ModifiedDate], [t1].[ContactID] AS [ContactID2], [t1].[NameStyle], [t1].[Title], [t1].[FirstName], [t1].[MiddleName], [t1].[LastName], [t1].[Suffix], [t1].[EmailAddress], [t1].[EmailPromotion], [t1].[Phone], [t1].[PasswordHash], [t1].[PasswordSalt], [t1].[AdditionalContactInfo], [t1].[rowguid] AS [rowguid2], [t1].[ModifiedDate] AS [ModifiedDate2]
2 FROM [Sales].[SalesOrderHeader] AS [t0]
3 INNER JOIN [Person].[Contact] AS [t1] ON [t1].[ContactID] = [t0].[ContactID]
Uma única viagem ao servidor. Era isso que eu buscava. Excelente!
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.