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:

linqtosqlAW

Vejam o resultado abaixo, na página SemPerformance.aspx:

semperf

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:

sqlprofiler

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:

sqlprofilercomperf

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.