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!


Postado na(s) categoria(s) LINQ pelo Giovanni Bassi em 24 de março de 2008 às 20:37 | Tags: , ,

Ninguém avaliou. Dê sua nota!

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comentários

Comentar


(Vai mostrar seu Gravatar)  

  Country flag

biuquote
  • Comentário
  • Pré-visualização
Loading



Quem é Giovanni Bassi

Giovanni Bassi Sou uma pessoa apaixonada por tecnologia e especificamente por .Net. Gerencio uma fábrica de software, gosto muito de arquitetura e engenharia de software, publico artigos e edito a .Net Magazine. Dou umas palestras e cursos de vez em quando, e quando dá tempo eu respiro um pouco. Mais detalhes nesta página.

Selos

Web Days 2008

MCPD

MCSD

.Net Magazine

Calendário

«  agosto 2008  »
seteququsedo
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567
Ver detalhamento de posts no calendário

Postagens recentes

Comentários recentes

Disclaimer / Aviso
As opiniões colocadas neste blog são minhas e pessoais e não expressam necessariamente as opiniões de meus empregadores, pareceiros e amigos. Da mesma forma, os comentários feitos por leitores do blog não expressam a minha opinião.

© Copyright 2008 .Net Unplugged
Sign in