Partilhar via


LINQ to SQL: consulta integrada à linguagem do .NET para dados relacionais

 

Dinesh Kulkarni, Luca Bolognese, Matt Warren, Anders Hejlsberg, Kit George

Março de 2007

Aplica-se a:
   Visual Studio Code nome "Orcas"
   .Net Framework 3.5

Resumo: LINQ to SQL fornece uma infraestrutura de runtime para gerenciar dados relacionais como objetos sem perder a capacidade de consultar. Seu aplicativo é livre para manipular os objetos enquanto LINQ to SQL permanece em segundo plano acompanhando suas alterações automaticamente. (119 páginas impressas)

Sumário

Introdução
Um Tour Rápido
   Criando classes de entidade
   O DataContext
   Definindo Relações
   Consulte através das relações
   Modificando e salvando entidades
Consultas In-Depth
   Execução da consulta
   Identidade do objeto
   Relações
   Junções
   Projeções
   Consultas compiladas
   Tradução de SQL
O ciclo de vida da entidade
   Controle de alterações
   Enviando alterações
   Alterações simultâneas
   Transactions
   Procedimentos armazenados
Classes de entidade In-Depth
   Usando atributos
   Consistência do Grafo
   Alterar Notificações
   Herança
Tópicos avançados
   Criando bancos de dados
   Interoperando com ADO.NET
   Alterar resolução de conflitos
   Invocação de Procedimentos Armazenados
   A ferramenta gerador de classe de entidade
   Referência DBML da ferramenta geradora
   Entidades de várias camadas
   Mapeamento externo
   Notas e suporte a funções do NET Framework
   Depuração de suporte

Introdução

A maioria dos programas escritos hoje manipulam dados de uma forma ou de outra e, muitas vezes, esses dados são armazenados em um banco de dados relacional. No entanto, há uma enorme divisão entre linguagens de programação modernas e bancos de dados na forma como eles representam e manipulam informações. Essa incompatibilidade de impedância é visível de várias maneiras. O mais notável é que as linguagens de programação acessam informações em bancos de dados por meio de APIs que exigem que as consultas sejam especificadas como cadeias de caracteres de texto. Essas consultas são partes significativas da lógica do programa. No entanto, eles são opacos para a linguagem, não é possível se beneficiar de recursos de tempo de compilação e tempo de design, como o IntelliSense.

Claro, as diferenças vão muito mais fundo do que isso. A forma como as informações são representadas — o modelo de dados — é bem diferente entre os dois. Linguagens de programação modernas definem informações na forma de objetos. Os bancos de dados relacionais usam linhas. Os objetos têm uma identidade exclusiva, pois cada instância é fisicamente diferente de outra. As linhas são identificadas por valores de chave primária. Os objetos têm referências que identificam e vinculam instâncias. As linhas são deixadas intencionalmente distintas exigindo que as linhas relacionadas sejam amarradas livremente usando chaves estrangeiras. Os objetos permanecem sozinhos, existentes desde que ainda sejam referenciados por outro objeto. As linhas existem como elementos de tabelas, desaparecendo assim que são removidas.

Não é à toa que os aplicativos esperados para preencher essa lacuna são difíceis de criar e manter. Certamente simplificaria a equação para se livrar de um lado ou de outro. No entanto, os bancos de dados relacionais fornecem infraestrutura crítica para processamento de consultas e armazenamento de longo prazo e linguagens de programação modernas são indispensáveis para desenvolvimento ágil e computação avançada.

Até agora, foi trabalho do desenvolvedor de aplicativos resolve essa incompatibilidade em cada aplicativo separadamente. As melhores soluções até agora foram camadas de abstração de banco de dados elaboradas que transportam as informações entre os modelos de objeto específicos do domínio de aplicativos e a representação tabular do banco de dados, remodelando e reformatando os dados de cada maneira. No entanto, ao ocultar a verdadeira fonte de dados, essas soluções acabam jogando fora o recurso mais atraente dos bancos de dados relacionais; a capacidade dos dados serem consultados.

LINQ to SQL, um componente do nome Visual Studio Code "Orcas", fornece uma infraestrutura em tempo de execução para gerenciar dados relacionais como objetos sem perder a capacidade de consultar. Ele faz isso convertendo consultas integradas à linguagem em SQL para execução pelo banco de dados e, em seguida, convertendo os resultados tabulares de volta em objetos definidos por você. Em seguida, seu aplicativo é livre para manipular os objetos enquanto LINQ to SQL permanece em segundo plano acompanhando suas alterações automaticamente.

  • LINQ to SQL foi projetado para não ser intrusivo ao seu aplicativo.
    • É possível migrar soluções de ADO.NET atuais para LINQ to SQL de forma por etapas (compartilhando as mesmas conexões e transações), uma vez que LINQ to SQL é simplesmente outro componente na família ADO.NET. LINQ to SQL também tem amplo suporte para procedimentos armazenados, permitindo a reutilização dos ativos corporativos existentes.
  • LINQ to SQL aplicativos são fáceis de começar.
    • Objetos vinculados a dados relacionais podem ser definidos como objetos normais, decorados apenas com atributos para identificar como as propriedades correspondem às colunas. Claro, nem é necessário fazer isso manualmente. Uma ferramenta de tempo de design é fornecida para automatizar a tradução de esquemas de banco de dados relacionais pré-existentes em definições de objeto para você.

Juntos, o LINQ to SQL infraestrutura de tempo de execução e ferramentas de tempo de design reduzem significativamente a carga de trabalho para o desenvolvedor de aplicativos de banco de dados. Os capítulos a seguir fornecem uma visão geral de como LINQ to SQL podem ser usados para executar tarefas comuns relacionadas ao banco de dados. Supõe-se que o leitor esteja familiarizado com Language-Integrated Consulta e os operadores de consulta padrão.

LINQ to SQL é independente da linguagem. Qualquer linguagem criada para fornecer Language-Integrated Consulta pode usá-la para habilitar o acesso a informações armazenadas em bancos de dados relacionais. Os exemplos neste documento são mostrados no C# e no Visual Basic; LINQ to SQL também pode ser usado com a versão habilitada para LINQ do compilador do Visual Basic.

Um Tour Rápido

A primeira etapa na criação de um aplicativo LINQ to SQL é declarar as classes de objeto que você usará para representar os dados do aplicativo. Vamos analisar um exemplo.

Criando classes de entidade

Começaremos com uma classe simples Customer e a associaremos à tabela de clientes no banco de dados de exemplo Northwind. Para fazer isso, precisamos aplicar apenas um atributo personalizado à parte superior da declaração de classe. LINQ to SQL define o atributo Table para essa finalidade.

C#

[Table(Name="Customers")]
public class Customer
{
   public string CustomerID;
   public string City;
}

Visual Basic

<Table(Name:="Customers")> _
Public Class Customer
   Public CustomerID As String
   Public City As String
End Class

O atributo Table tem uma propriedade Name que você pode usar para especificar o nome exato da tabela de banco de dados. Se nenhuma propriedade Name for fornecida, LINQ to SQL assumirá que a tabela de banco de dados tem o mesmo nome que a classe . Somente instâncias de classes declaradas como tabelas serão armazenadas no banco de dados. Instâncias desses tipos de classes são conhecidas como entidades. As próprias classes são conhecidas como classes de entidade.

Além de associar classes a tabelas, você precisará denotar cada campo ou propriedade que pretende associar a uma coluna de banco de dados. Para isso, LINQ to SQL define o atributo Column.

C#

[Table(Name="Customers")]
public class Customer
{
   [Column(IsPrimaryKey=true)]
   public string CustomerID;
   [Column]
   public string City;
}

Visual Basic

<Table(Name:="Customers")> _
Public Class Customer
   <Column(IsPrimaryKey:=true)> _
   Public CustomerID As String

   <Column> _
   Public City As String

End Class

O atributo Column tem uma variedade de propriedades que você pode usar para personalizar o mapeamento exato entre os campos e as colunas do banco de dados. Uma propriedade de observação é a propriedade Id . Ele informa LINQ to SQL que a coluna de banco de dados faz parte da chave primária na tabela.

Assim como acontece com o atributo Table , você só precisa fornecer informações no atributo Column se ele for diferente do que pode ser deduzido da declaração de campo ou propriedade. Neste exemplo, você precisa informar a LINQ to SQL que o campo CustomerID faz parte da chave primária na tabela, mas não precisa especificar o nome ou tipo exato.

Somente campos e propriedades declarados como colunas serão persistidos ou recuperados do banco de dados. Outras serão consideradas como partes transitórias da lógica do aplicativo.

O DataContext

O DataContext é o canal main pelo qual você recupera objetos do banco de dados e reenvia as alterações. Use-o da mesma maneira que usaria uma Conexão ADO.NET. Na verdade, o DataContext é inicializado com uma conexão ou cadeia de conexão que você fornece. A finalidade do DataContext é converter suas solicitações de objetos em consultas SQL feitas no banco de dados e, em seguida, montar objetos fora dos resultados. O DataContext habilita a consulta integrada à linguagem implementando o mesmo padrão de operador que os operadores de consulta padrão , como Where e Select.

Por exemplo, você pode usar o DataContext para recuperar objetos de cliente cuja cidade é Londres da seguinte maneira:

C#

// DataContext takes a connection string 
DataContext db = new   DataContext("c:\\northwind\\northwnd.mdf");
// Get a typed table to run queries
Table<Customer> Customers = db.GetTable<Customer>();
// Query for customers from London
var q =
   from c in Customers
   where c.City == "London"
   select c;
foreach (var cust in q)
   Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City);

Visual Basic

' DataContext takes a connection string 
Dim db As DataContext  = New DataContext("c:\northwind\northwnd.mdf")
' Get a typed table to run queries
Dim Customers As Customers(Of Customer) = db.GetTable(Of Customer)()
' Query for customers from London
Dim londonCustomers = From customer in Customers _
                      Where customer.City = "London" _
                      Select customer
For Each cust in londonCustomers
   Console.WriteLine("id = " & cust.CustomerID & ", City = " & cust.City)
Next

Cada tabela de banco de dados é representada como uma coleção Table , acessível por meio do método GetTable() usando sua classe de entidade para identificá-la. É recomendável declarar um DataContext fortemente tipado em vez de depender da classe Básica DataContext e do método GetTable(). Um DataContext fortemente tipado declara todas as coleções table como membros do contexto.

C#

public partial class Northwind : DataContext
{
   public Table<Customer> Customers;
   public Table<Order> Orders;
   public Northwind(string connection): base(connection) {}
}

Visual Basic

Partial Public Class Northwind 
              Inherits DataContext

   Public Customers As Table(Of Customers)
   Public Orders As Table(Of Orders)
         Public Sub New(ByVal connection As String)
            MyBase.New(connection)
   End Sub
End Class

A consulta para clientes de Londres pode ser expressa de forma mais simples como:

C#

Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
var q =
   from c in db.Customers
   where c.City == "London"
   select c;
foreach (var cust in q)
   Console.WriteLine("id = {0}, City = {1}",cust.CustomerID, cust.City);

Visual Basic

Dim db = New Northwind("c:\northwind\northwnd.mdf")
Dim londonCustomers = From cust In db.Customers _
                      Where cust.City = "London" _
                      Select cust
For Each cust in londonCustomers
   Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City) 

Next

Continuaremos a usar a classe Northwind fortemente tipada para o restante do documento de visão geral.

Definindo Relações

As relações em bancos de dados relacionais normalmente são modeladas como valores de chave estrangeira que se referem a chaves primárias em outras tabelas. Para navegar entre elas, você deve reunir explicitamente as duas tabelas usando uma operação de junção relacional. Os objetos, por outro lado, referem-se uns aos outros usando referências de propriedade ou coleções de referências navegadas usando notação "ponto". Obviamente, a dotação é mais simples do que ingressar, pois você não precisa se lembrar da condição de junção explícita sempre que navegar.

Para relações de dados como essas que sempre serão as mesmas, torna-se bastante conveniente codificá-las como referências de propriedade em sua classe de entidade. LINQ to SQL define um atributo Association que você pode aplicar a um membro usado para representar uma relação. Uma relação de associação é como uma relação de chave estrangeira para chave primária que é feita pela correspondência de valores de coluna entre tabelas.

C#

[Table(Name="Customers")]
public class Customer
{
   [Column(Id=true)]
   public string CustomerID;
   ...
   private EntitySet<Order> _Orders;
   [Association(Storage="_Orders", OtherKey="CustomerID")]
   public EntitySet<Order> Orders {
      get { return this._Orders; }
      set { this._Orders.Assign(value); }
   }
}

Visual Basic

<Table(Name:="Customers")> _
Public Class Customer

   <Column(Id:=true)> _
   Public CustomerID As String
   ...
   Private _Orders As EntitySet(Of Order)
                  <Association(Storage:="_Orders", OtherKey:="CustomerID")> _
         Public Property Orders() As EntitySet(Of Order)
            Get
               Return Me._Orders
            End Get
            Set(ByVal value As EntitySet(Of Order))
            End Set
   End Property

End Class

A classe Customer agora tem uma propriedade que declara a relação entre os clientes e seus pedidos. A propriedade Orders é do tipo EntitySet porque a relação é um para muitos. Usamos a propriedade OtherKey no atributo Association para descrever como essa associação é feita. Ele especifica os nomes das propriedades na classe relacionada a serem comparadas com esta. Havia também uma propriedade ThisKey que não especificamos. Normalmente, o usaríamos para listar os membros desse lado da relação. No entanto, ao omiti-lo, permitimos que LINQ to SQL inferi-los dos membros que compõem a chave primária.

Observe como isso é invertido na definição da classe Order .

C#

[Table(Name="Orders")]
public class Order
{
   [Column(Id=true)]
   public int OrderID;
   [Column]
   public string CustomerID;
   private EntityRef<Customer> _Customer;    
   [Association(Storage="_Customer", ThisKey="CustomerID")]
   public Customer Customer {
      get { return this._Customer.Entity; }
      set { this._Customer.Entity = value; }
   }
}

Visual Basic

<Table(Name:="Orders")> _
Public Class Order

   <Column(Id:=true)> _
   Public OrderID As String
   <Column> _
   Public CustomerID As String
   Private _Customer As EntityRef(Of Customer)
         <Association(Storage:="_Customer", ThisKey:="CustomerID")> _
         Public Property Customer() As Customer
            Get
               Return Me._Customer.Entity
            End Get
            Set(ByVal value As Customer)
               Me._Customers.Entity = value
            End Set
   End Property
End Class

A classe Order usa o tipo EntityRef para descrever a relação com o cliente. O uso da classe EntityRef é necessário para dar suporte ao carregamento adiado (discutido posteriormente). O atributo Association para a propriedade Customer especifica a propriedade ThisKey , pois os membros não inferíveis agora estão nesse lado da relação.

Dê uma olhada também na propriedade Storage . Ele informa LINQ to SQL qual membro privado é usado para manter o valor da propriedade. Isso permite que LINQ to SQL ignorem seus acessadores de propriedade pública quando armazenam e recuperam seu valor. Isso é essencial se você quiser LINQ to SQL para evitar qualquer lógica de negócios personalizada escrita em seus acessadores. Se a propriedade de armazenamento não for especificada, os acessadores públicos serão usados. Você também pode usar a propriedade Storage com atributos Column .

Depois de introduzir relações em suas classes de entidade, a quantidade de código que você precisa escrever aumenta à medida que você introduz suporte para notificações e consistência de grafo. Felizmente, há uma ferramenta (descrita posteriormente) que pode ser usada para gerar todas as definições necessárias como classes parciais, permitindo que você use uma combinação de código gerado e lógica de negócios personalizada.

Para o restante deste documento, presumimos que a ferramenta tenha sido usada para gerar um contexto de dados Northwind completo e todas as classes de entidade.

Consulte através das relações

Agora que você tem relações, pode usá-las ao escrever consultas simplesmente referindo-se às propriedades de relação definidas em sua classe.

C#

var q =
   from c in db.Customers
   from o in c.Orders
   where c.City == "London"
   select new { c, o };

Visual Basic

Dim londonCustOrders = From cust In db.Customers, ord In cust.Orders _
                       Where cust.City = "London" _
                       Select Customer = cust, Order = ord

A consulta acima usa a propriedade Orders para formar o produto cruzado entre clientes e pedidos, produzindo uma nova sequência de pares Cliente e Pedido .

Também é possível fazer o inverso.

C#

var q =
   from o in db.Orders
   where o.Customer.City == "London"
   select new { c = o.Customer, o };

Visual Basic

Dim londonCustOrders = From ord In db.Orders _
                       Where ord.Customer.City = "London" _
                       Select Customer = ord.Customer, Order = ord

Neste exemplo, os pedidos são consultados e a relação cliente é usada para acessar informações sobre o objeto Customer associado.

Modificando e salvando entidades

Poucos aplicativos são criados com apenas a consulta em mente. Os dados também devem ser criados e modificados. LINQ to SQL foi projetado para oferecer máxima flexibilidade na manipulação e persistência de alterações feitas em seus objetos. Assim que os objetos de entidade estiverem disponíveis, seja recuperando-os por meio de uma consulta ou construindo-os novamente, você poderá manipulá-los como objetos normais em seu aplicativo, alterando seus valores ou adicionando-os e removendo-os de coleções como desejar. LINQ to SQL rastreia todas as alterações e está pronta para transmiti-las de volta para o banco de dados assim que terminar.

O exemplo a seguir usa as classes Customer e Order geradas por uma ferramenta a partir dos metadados de todo o banco de dados de exemplo Northwind. As definições de classe não foram mostradas para fins de brevidade.

C#

Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// Query for a specific customer
string id = "ALFKI";
var cust = db.Customers.Single(c => c.CustomerID == id);
// Change the name of the contact
cust.ContactName = "New Contact";
// Create and add a new Order to Orders collection
Order ord = new Order { OrderDate = DateTime.Now };
cust.Orders.Add(ord);
// Ask the DataContext to save all the changes
db.SubmitChanges();

Visual Basic

Dim db As New Northwind("c:\northwind\northwnd.mdf")
' Query for a specific customer
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _ 
                     Where cust.CustomerID = id).First
' Change the name of the contact
targetCustomer.ContactName = "New Contact"
' Create and add a new Order to Orders collection
Dim id = New Order With { .OrderDate = DateTime.Now }
targetCustomer.Orders.Add(ord)
' Ask the DataContext to save all the changes
db.SubmitChanges()

Quando SubmitChanges() é chamado, LINQ to SQL gera e executa automaticamente comandos SQL para transmitir as alterações de volta para o banco de dados. Também é possível substituir esse comportamento pela lógica personalizada. A lógica personalizada pode chamar um procedimento armazenado de banco de dados.

Consultas In-Depth

LINQ to SQL fornece uma implementação dos operadores de consulta padrão para objetos associados a tabelas em um banco de dados relacional. Este capítulo descreve os aspectos específicos LINQ to SQL das consultas.

Execução da consulta

Se você escrever uma consulta como uma expressão de consulta de alto nível ou compilar uma com base nos operadores individuais, a consulta que você escreve não é uma instrução imperativa executada imediatamente. É uma descrição. Por exemplo, na declaração abaixo da variável local q refere-se à descrição da consulta e não ao resultado da execução dela.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c;
foreach (Customer c in q)
   Console.WriteLine(c.CompanyName);

Visual Basic

Dim londonCustomers = From cust In db.Customers _
                       where cust.City = "London"
For Each cust  In londonCustomers
   Console.WriteLine(cust.CompanyName) 
Next

O tipo real de q nesta instância é Cliente IQueryable<>. Não é até que o aplicativo tente enumerar o conteúdo da consulta que ele realmente executa. Neste exemplo, a instrução foreach faz com que a execução ocorra.

Um objeto IQueryable é semelhante a um objeto de comando ADO.NET. Ter uma em mãos não implica que uma consulta foi executada. Um objeto de comando mantém uma cadeia de caracteres que descreve uma consulta. Da mesma forma, um objeto IQueryable mantém uma descrição de uma consulta codificada como uma estrutura de dados conhecida como expressão. Um objeto de comando tem um método ExecuteReader() que causa a execução, retornando resultados como um DataReader. Um objeto IQueryable tem um método GetEnumerator() que causa a execução, retornando resultados como um Cliente> IEnumerator<.

Portanto, segue-se que, se uma consulta for enumerada duas vezes, ela será executada duas vezes.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c;
// Execute first time
foreach (Customer c in q)
   Console.WriteLine(c.CompanyName);
// Execute second time
foreach (Customer c in q)
   Console.WriteLine(c.CompanyName);

Visual Basic

Dim londonCustomers = From cust In db.Customers _
                       where cust.City = "London"
' Execute first time
For Each cust In londonCustomers
   Console.WriteLine(cust.CompanyName) 
Next
' Execute second time
For Each cust In londonCustomers
   Console.WriteLine(cust.CustomerID) 
Next

Esse comportamento é conhecido como execução adiada. Assim como acontece com um objeto de comando ADO.NET, é possível manter uma consulta e executá-la novamente.

É claro que os gravadores de aplicativos geralmente precisam ser muito explícitos sobre onde e quando uma consulta é executada. Seria inesperado se um aplicativo executasse uma consulta várias vezes simplesmente porque precisava examinar os resultados mais de uma vez. Por exemplo, talvez você queira associar os resultados de uma consulta a algo como um DataGrid. O controle pode enumerar os resultados sempre que pinta na tela.

Para evitar a execução várias vezes, converta os resultados em qualquer número de classes de coleção padrão. É fácil converter os resultados em uma lista ou matriz usando os operadores de consulta padrão ToList() ou ToArray().

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c;
// Execute once using ToList() or ToArray()
var list = q.ToList();
foreach (Customer c in list)
   Console.WriteLine(c.CompanyName);
foreach (Customer c in list)
   Console.WriteLine(c.CompanyName);

Visual Basic

Dim londonCustomers = From cust In db.Customers _
                       where cust.City = "London"
' Execute once using ToList() or ToArray()
Dim londonCustList = londonCustomers.ToList()
' Neither of these iterations re-executes the query
For Each cust In londonCustList
   Console.WriteLine(cust.CompanyName)
Next
For Each cust In londonCustList
   Console.WriteLine(cust.CompanyName)
Next 

Um benefício da execução adiada é que as consultas podem ser construídas por etapas com a execução ocorrendo somente quando a construção é concluída. Você pode começar compondo uma parte de uma consulta, atribuindo-a a uma variável local e, em algum momento depois, continuar aplicando mais operadores a ela.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c;
if (orderByLocation) {
   q =
      from c in q
      orderby c.Country, c.City
      select c;
}
else if (orderByName) {
   q =
      from c in q
      orderby c.ContactName
      select c;
}
foreach (Customer c in q)
   Console.WriteLine(c.CompanyName);

Visual Basic

Dim londonCustomers = From cust In db.Customers _
                       where cust.City = "London"
if orderByLocation Then
   londonCustomers = From cust in londonCustomers _
                     Order By cust.Country, cust.City

Else If orderByName Then
   londonCustomers = From cust in londonCustomers _
                     Order By cust.ContactName
End If
For Each cust In londonCustList
   Console.WriteLine(cust.CompanyName)
Next 

Neste exemplo, q começa como uma consulta para todos os clientes em Londres. Posteriormente, ele muda para uma consulta ordenada, dependendo do estado do aplicativo. Ao adiar a execução, a consulta pode ser construída para atender às necessidades exatas do aplicativo sem exigir manipulação de cadeia de caracteres arriscada.

Identidade do objeto

Os objetos no runtime têm identidade exclusiva. Se duas variáveis se referirem ao mesmo objeto, elas estarão realmente se referindo à mesma instância de objeto. Por isso, as alterações feitas por meio de um caminho por meio de uma variável são imediatamente visíveis por meio da outra. As linhas em uma tabela de banco de dados relacional não têm identidade exclusiva. No entanto, eles têm uma chave primária e essa chave primária pode ser exclusiva, o que significa que nenhuma duas linhas podem compartilhar a mesma chave. No entanto, isso restringe apenas o conteúdo da tabela de banco de dados. Portanto, desde que interajamos apenas com os dados por meio de comandos remotos, isso equivale a quase a mesma coisa.

No entanto, esse raramente é o caso. Na maioria das vezes, os dados são trazidos para fora do banco de dados e para uma camada diferente em que um aplicativo os manipula. Claramente, esse é o modelo que LINQ to SQL foi projetado para dar suporte. Quando os dados são retirados do banco de dados como linhas, não há expectativa de que duas linhas que representam os mesmos dados realmente correspondam às mesmas instâncias de linha. Se você consultar um cliente específico duas vezes, obterá duas linhas de dados, cada uma contendo as mesmas informações.

No entanto, com objetos, você espera algo bem diferente. Você espera que, se você solicitar ao DataContext as mesmas informações novamente, ele de fato devolverá a mesma instância de objeto. Você espera isso porque os objetos têm um significado especial para seu aplicativo e você espera que eles se comportem como objetos normais. Você os projetou como hierarquias ou grafos e certamente espera recuperá-los como tal, sem hordas de instâncias replicadas apenas porque você pediu a mesma coisa duas vezes.

Por isso, o DataContext gerencia a identidade do objeto. Sempre que uma nova linha é recuperada do banco de dados, ela é registrada em uma tabela de identidade por sua chave primária e um novo objeto é criado. Sempre que essa mesma linha for recuperada novamente, a instância de objeto original será devolvida ao aplicativo. Dessa forma, o DataContext converte o conceito de identidade (chaves) dos bancos de dados no conceito de linguagens (instâncias). O aplicativo só vê o objeto no estado em que foi recuperado pela primeira vez. Os novos dados, se diferentes, são descartados.

Você pode ficar intrigado com isso, pois por que algum aplicativo jogaria dados fora? Acontece que é assim que LINQ to SQL gerencia a integridade dos objetos locais e é capaz de dar suporte a atualizações otimistas. Como as únicas alterações que ocorrem depois que o objeto é criado inicialmente são aquelas feitas pelo aplicativo, a intenção do aplicativo é clara. Se as alterações feitas por uma parte externa tiverem ocorrido nesse ínterim, elas serão identificadas no momento em que SubmitChanges() for chamado. Mais disso é explicado na seção Alterações Simultâneas.

Observe que, caso o banco de dados contenha uma tabela sem uma chave primária, LINQ to SQL permite que consultas sejam enviadas pela tabela, mas não permite atualizações. Isso ocorre porque a estrutura não pode identificar qual linha atualizar, dada a falta de uma chave exclusiva.

É claro que, se o objeto solicitado pela consulta for facilmente identificável por sua chave primária, pois uma já recuperada, nenhuma consulta será executada. A tabela de identidade atua como um cache que armazena todos os objetos recuperados anteriormente.

Relações

Como vimos no tour rápido, as referências a outros objetos ou coleções de outros objetos em suas definições de classe correspondem diretamente a relações de chave estrangeira no banco de dados. Você pode usar essas relações ao consultar simplesmente usando a notação de ponto para acessar as propriedades da relação, navegando de um objeto para outro. Essas operações de acesso se traduzem em junções mais complicadas ou subconsultas correlacionadas no SQL equivalente, permitindo que você percorra o grafo de objeto durante uma consulta. Por exemplo, a consulta a seguir navega pedidos para clientes como uma maneira de restringir os resultados apenas 2 os pedidos de clientes localizados em Londres.

C#

var q =
   from o in db.Orders
   where o.Customer.City == "London"
   select o;

Visual Basic

Dim londonOrders = From ord In db.Orders _
                       where ord.Customer.City = "London"

Se as propriedades de relação não existissem, você teria que gravá-las manualmente como junções, assim como faria em uma consulta SQL.

C#

var q =
   from c in db.Customers
   join o in db.Orders on c.CustomerID equals o.CustomerID
   where c.City == "London"
   select o;

Visual Basic

Dim londonOrders = From cust In db.Customers _
                            Join ord In db.Orders _
                            On cust.CustomerID Equals ord.CustomerID _
                   Where ord.Customer.City = "London" _
                   Select ord

A propriedade relationship permite que você defina essa relação específica depois de habilitar o uso da sintaxe de ponto mais conveniente. No entanto, esse não é o motivo pelo qual as propriedades de relação existem. Eles existem porque tendemos a definir nossos modelos de objeto específicos do domínio como hierarquias ou grafos. Os objetos nos quais escolhemos programar têm referências a outros objetos. É apenas uma feliz coincidência que, como as relações objeto a objeto correspondem a relações de estilo de chave estrangeira em bancos de dados, o acesso à propriedade leva a uma maneira conveniente de gravar junções.

Portanto, a existência de propriedades de relação é mais importante no lado dos resultados de uma consulta do que como parte da própria consulta. Depois de colocar as mãos em um cliente específico, sua definição de classe informa que os clientes têm pedidos. Portanto, ao examinar a propriedade Orders de um cliente específico, você espera ver a coleção preenchida com todos os pedidos do cliente, pois esse é, na verdade, o contrato que você declarou definindo as classes dessa maneira. Você espera ver as ordens lá mesmo que você não tenha pedido pedidos antecipadamente. Você espera que seu modelo de objeto mantenha uma ilusão de que ele é uma extensão na memória do banco de dados, com objetos relacionados imediatamente disponíveis.

LINQ to SQL implementa uma técnica chamada carregamento adiado para ajudar a manter essa ilusão. Ao consultar um objeto, na verdade, você só recupera os objetos solicitados. Os objetos relacionados não são buscados automaticamente ao mesmo tempo. No entanto, o fato de que os objetos relacionados ainda não estão carregados não é observável, pois assim que você tenta acessá-los, uma solicitação sai para recuperá-los.

C#

var q =
   from o in db.Orders
   where o.ShipVia == 3
   select o;
foreach (Order o in q) {
   if (o.Freight > 200)
      SendCustomerNotification(o.Customer);
   ProcessOrder(o);
}

Visual Basic

Dim shippedOrders = From ord In db.Orders _
                    where ord.ShipVia = 3
For Each ord In shippedOrders
   If ord.Freight > 200 Then
      SendCustomerNotification(ord.Customer) 
      ProcessOrder(ord)
   End If
Next

Por exemplo, talvez você queira consultar um determinado conjunto de pedidos e, em seguida, apenas ocasionalmente enviar uma notificação por email para clientes específicos. Você não precisaria recuperar todos os dados do cliente antecipadamente com cada pedido. O carregamento adiado permite adiar o custo de recuperação de informações extras até que seja absolutamente necessário.

Claro, o oposto também pode ser verdade. Você pode ter um aplicativo que precisa examinar os dados do cliente e do pedido ao mesmo tempo. Você conhece-o necessidade dois conjuntos de dados. Você sabe que seu aplicativo fará uma busca detalhada nos pedidos de cada cliente assim que obtê-los. Seria lamentável disparar consultas individuais para pedidos para cada cliente. O que você realmente quer que aconteça é ter os dados do pedido recuperados junto com os clientes.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c;
foreach (Customer c in q) {
   foreach (Order o in c.Orders) {
      ProcessCustomerOrder(o);
   }
}

Visual Basic

Dim londonCustomers = From cust In db.Customer _
                   Where cust.City = "London"
For Each cust In londonCustomers
   For Each ord In cust.Orders
      ProcessCustomerOrder(ord) 
   End If
Next

Certamente, você sempre pode encontrar uma maneira de unir clientes e pedidos em uma consulta formando o produto cruzado e recuperando todos os bits relativos de dados como uma grande projeção. Mas os resultados não seriam entidades. Entidades são objetos com identidade que você pode modificar, enquanto os resultados seriam projeções que não podem ser alteradas e persistidas. Pior, você estaria recuperando uma enorme quantidade de dados redundantes à medida que cada cliente se repete para cada pedido na saída de junção nivelada.

O que você realmente precisa é de uma maneira de recuperar um conjunto de objetos relacionados ao mesmo tempo— uma parte delineada de um grafo para que você nunca recuperasse mais ou menos do que o necessário para o uso pretendido.

LINQ to SQL permite solicitar o carregamento imediato de uma região do modelo de objeto apenas por esse motivo. Ele faz isso permitindo a especificação de um DataShape para um DataContext. A classe DataShape é usada para instruir a estrutura sobre quais objetos recuperar quando um tipo específico é recuperado. Isso é feito usando o método LoadWith como no seguinte:

C#

DataShape ds = new DataShape();
ds.LoadWith<Customer>(c => c.Orders);
db.Shape = ds;
var q = 
   from c in db.Customers
   where c.City == "London"
   select c;

Visual Basic

Dim ds As DataShape = New DataShape()
ds.LoadWith(Of Customer)(Function(c As Customer) c.Orders)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
                      Where cust.City = "London" _
                      Select cust

Na consulta anterior, todos os Pedidos para todos os clientes que residem em Londres são recuperados quando a consulta é executada, de modo que o acesso sucessivo à propriedade Orders em um objeto Customer não dispare uma consulta de banco de dados.

A classe DataShape também pode ser usada para especificar subconsultas que são aplicadas a uma navegação de relação. Por exemplo, se você quiser recuperar apenas os Pedidos que foram enviados hoje, poderá usar o método AssociateWith no DataShape como no seguinte:

C#

DataShape ds = new DataShape();
ds.AssociateWith<Customer>(
   c => c.Orders.Where(p => p.ShippedDate != DateTime.Today));
db.Shape = ds;
var q = 
   from c in db.Customers
   where c.City == "London"
   select c;
foreach(Customer c in q) {
   foreach(Order o in c.Orders) {}
}

Visual Basic

Dim ds As DataShape = New DataShape()
ds.AssociateWith(Of Customer)( _
         Function(cust As Customer) From cust In db.Customers _
                                 Where order.ShippedDate <> Today _
                                 Select cust)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
                      Where cust.City = "London" _
                      Select cust
For Each cust in londonCustomers
   For Each ord In cust.Orders …
   Next
   Next

No código anterior, a instrução foreach interna itera logo acima dos Pedidos que foram enviados hoje, pois apenas esses pedidos foram recuperados do banco de dados.

É importante observar dois fatos sobre a classe DataShape :

  1. Depois de atribuir um DataShape a um DataContext, o DataShape não pode ser modificado. Qualquer chamada de método LoadWith ou AssociateWith em tal DataShape retornará um erro em tempo de execução.

  2. É impossível criar ciclos usando LoadWith ou AssociateWith. Por exemplo, o seguinte gera um erro em tempo de execução:

    C#

    DataShape ds = new DataShape();
    ds.AssociateWith<Customer>(
             c=>c.Orders.Where(o=> o.Customer.Orders.Count() < 35);
    

    Visual Basic

    Dim ds As DataShape = New DataShape()
    ds.AssociateWith(Of Customer)( _
             Function(cust As Customer) From ord In cust.Orders _
                          Where ord.Customer.Orders.Count() < 35)
    

Junções

A maioria das consultas em modelos de objeto depende muito da navegação de referências de objeto no modelo de objeto. No entanto, há "relações" interessantes entre entidades que podem não ser capturadas no modelo de objeto como referências. Por exemplo , Customer.Orders é uma relação útil baseada em relações de chave estrangeira no banco de dados Northwind. No entanto, Fornecedores e Clientes na mesma Cidade ou País é uma relação ad hoc que não se baseia em uma relação de chave estrangeira e pode não ser capturada no modelo de objeto. As junções fornecem um mecanismo adicional para lidar com essas relações. LINQ to SQL dá suporte aos novos operadores de junção introduzidos no LINQ.

Considere o seguinte problema: encontre fornecedores e clientes baseados na mesma cidade. A consulta a seguir retorna nomes de empresas de fornecedores e clientes e a cidade comum como resultado mesclado. Isso é o equivalente à junção de equi interna em bancos de dados relacionais:

C#

var q = 
   from s in db.Suppliers
   join c in db.Customers on s.City equals c.City
   select new {
      Supplier = s.CompanyName,
      Customer = c.CompanyName,
      City = c.City
   };

Visual Basic

Dim customerSuppliers = From sup In db.Suppliers _
                        Join cust In db.Customers _
                              On sup.City Equals cust.City _
                        Select Supplier = sup.CompanyName, _
                        CustomerName = cust.CompanyName, _
                        City = cust.City

A consulta acima elimina os fornecedores que não estão na mesma cidade que um determinado cliente. No entanto, há momentos em que não queremos eliminar uma das entidades em uma relação ad hoc . A consulta a seguir lista todos os fornecedores com grupos de clientes para cada um dos fornecedores. Se um fornecedor específico não tiver nenhum cliente na mesma cidade, o resultado será uma coleção vazia de clientes correspondentes a esse fornecedor. Observe que os resultados não são simples— cada fornecedor tem uma coleção associada. Efetivamente, isso fornece junção de grupo – ele une duas sequências e agrupa elementos da segunda sequência pelos elementos da primeira sequência.

C#

var q = 
   from s in db.Suppliers
   join c in db.Customers on s.City equals c.City into scusts
   select new { s, scusts };

Visual Basic

Dim customerSuppliers = From sup In db.Suppliers _
                        Group Join cust In db.Customers _
                              On sup.City Equals cust.City _
                              Into supCusts _
                        Select Supplier = sup, _
                        Customers = supCusts

A junção de grupo também pode ser estendida para várias coleções. A consulta a seguir estende a consulta acima listando os funcionários que estão na mesma cidade que o fornecedor. Aqui, o resultado mostra um fornecedor com coleções (possivelmente vazias) de clientes e funcionários.

C#

var q = 
   from s in db.Suppliers
   join c in db.Customers on s.City equals c.City into scusts
   join e in db.Employees on s.City equals e.City into semps
   select new { s, scusts, semps };

Visual Basic

Dim customerSuppliers = From sup In db.Suppliers _
                        Group Join cust In db.Customers _
                              On sup.City Equals cust.City _
                              Into supCusts _
                        Group Join emp In db.Employees _
                              On sup.City Equals emp.City _
                              Into supEmps _
                        Select Supplier = sup, _
                        Customers = supCusts, Employees = supEmps

Os resultados de uma junção de grupo também podem ser mesclados. Os resultados da mesclagem da junção de grupo entre fornecedores e clientes são várias entradas para fornecedores com vários clientes em sua cidade — uma por cliente. Coleções vazias são substituídas por nulos. Isso é equivalente a uma junção de equi externa esquerda em bancos de dados relacionais.

C#

var q = 
   from s in db.Suppliers
   join c in db.Customers on s.City equals c.City into sc
   from x in sc.DefaultIfEmpty()
   select new {
      Supplier = s.CompanyName, 
      Customer = x.CompanyName, 
      City = x.City 
   };

Visual Basic

Dim customerSuppliers = From sup In db.Suppliers _
                        Group Join cust In db.Customers _
                              On sup.City Equals cust.City _
                              Into supCusts _
                        Select Supplier = sup, _
                        CustomerName = supCusts.CompanyName, sup.City

As assinaturas para operadores de junção subjacentes são definidas no documento operadores de consulta padrão. Há suporte apenas para junções de equi e os dois operandos de iguais devem ter o mesmo tipo.

Projeções

Até agora, examinamos apenas as consultas para recuperar entidades , objetos diretamente associados a tabelas de banco de dados. Não precisamos nos restringir a isso. A beleza de uma linguagem de consulta é que você pode recuperar informações de qualquer forma desejada. Você não poderá aproveitar o controle automático de alterações ou o gerenciamento de identidade quando fizer isso. No entanto, você pode obter apenas os dados desejados.

Por exemplo, talvez você simplesmente precise saber os nomes da empresa de todos os clientes em Londres. Se esse for o caso, não há nenhum motivo específico para recuperar objetos inteiros do cliente apenas para escolher nomes. Você pode projetar os nomes como parte da consulta.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select c.CompanyName;

Visual Basic

Dim londonCustomerNames = From cust In db.Customer _
                          Where cust.City = "London" _
                          Select cust.CompanyName

Nesse caso, q se torna uma consulta que recupera uma sequência de cadeias de caracteres.

Se você quiser obter mais do que apenas um único nome, mas não o suficiente para justificar a busca de todo o objeto do cliente, você pode especificar qualquer subconjunto desejado construindo os resultados como parte da consulta.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new { c.CompanyName, c.Phone };

Visual Basic

Dim londonCustomerInfo = From cust In db.Customer _
                         Where cust.City = "London" _
                         Select cust.CompanyName, cust.Phone

Este exemplo usa um inicializador de objeto anônimo para criar uma estrutura que contém o nome da empresa e o número de telefone. Talvez você não saiba como chamar o tipo, mas com a declaração de variável local digitada implicitamente no idioma que você não precisa necessariamente.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new { c.CompanyName, c.Phone };
foreach(var c in q)
   Console.WriteLine("{0}, {1}", c.CompanyName, c.Phone);

Visual Basic

Dim londonCustomerInfo = From cust In db.Customer _
                         Where cust.City = "London" _
                         Select cust.CompanyName, cust.Phone
For Each cust In londonCustomerInfo 
   Console.WriteLine(cust.CompanyName & ", " & cust.Phone) 
Next

Se você estiver consumindo os dados imediatamente, os tipos anônimos serão uma boa alternativa para definir explicitamente classes para manter os resultados da consulta.

Você também pode formar produtos cruzados de objetos inteiros, embora raramente tenha um motivo para fazer isso.

C#

var q =
   from c in db.Customers
   from o in c.Orders
   where c.City == "London"
   select new { c, o };

Visual Basic

Dim londonOrders = From cust In db.Customer, _
                   ord In db.Orders _
                   Where cust.City = "London" _
                   Select Customer = cust, Order = ord

Essa consulta constrói uma sequência de pares de objetos de pedido e cliente.

Também é possível fazer projeções em qualquer estágio da consulta. Você pode projetar dados em objetos recém-construídos e, em seguida, fazer referência aos membros desses objetos em operações de consulta subsequentes.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new {Name = c.ContactName, c.Phone} into x
   orderby x.Name
   select x;

Visual Basic

Dim londonItems = From cust In db.Customer _
                  Where cust.City = "London" _
                  Select Name = cust.ContactName, cust.Phone _
                  Order By Name

No entanto, tenha cuidado com o uso de construtores parametrizados nesta fase. Tecnicamente, é válido fazer isso, mas é impossível para LINQ to SQL acompanhar como o uso do construtor afeta o estado membro sem entender o código real dentro do construtor.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new MyType(c.ContactName, c.Phone) into x
   orderby x.Name
   select x;

Visual Basic

Dim londonItems = From cust In db.Customer _
                  Where cust.City = "London" _
                  Select MyType = New MyType(cust.ContactName, cust.Phone) _
                  Order By MyType.Name

Como LINQ to SQL tentativas de converter a consulta em tipos de objeto sql relacional puros definidos localmente não estão disponíveis no servidor para realmente construir. Toda a construção do objeto é, na verdade, adiada até que os dados sejam recuperados do banco de dados. No lugar de construtores reais, o SQL gerado usa a projeção de coluna SQL normal. Como não é possível que o tradutor de consultas entenda o que está acontecendo durante uma chamada de construtor, não é possível estabelecer um significado para o campo Nome de MyType.

Em vez disso, a melhor prática é sempre usar inicializadores de objeto para codificar projeções.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new MyType { Name = c.ContactName, HomePhone = c.Phone } into x
   orderby x.Name
   select x;

Visual Basic

Dim londonCustomers = From cust In db.Customer _
                      Where cust.City = "London" _
                      Select Contact = New With {.Name = cust.ContactName, _
                      .Phone = cust.Phone} _
                      Order By Contact.Name

O único local seguro para usar um construtor parametrizado está na projeção final de uma consulta.

C#

var e =
   new XElement("results",
      from c in db.Customers
      where c.City == "London"
      select new XElement("customer",
         new XElement("name", c.ContactName),
         new XElement("phone", c.Phone)
      )
   );

Visual Basic

      Dim x = <results>
                  <%= From cust In db.Customers _
                      Where cust.City = "London" _
                      Select <customer>
                         <name><%= cust.ContactName %></name>
                         <phone><%= cust.Phone %></phone>
                      </customer> 
                 %>
        </results>

Você pode até mesmo usar aninhamento elaborado de construtores de objetos se desejar, como este exemplo que constrói XML diretamente do resultado de uma consulta. Ele funciona desde que seja a última projeção da consulta.

Ainda assim, mesmo que as chamadas do construtor sejam compreendidas, as chamadas para métodos locais podem não ser. Se sua projeção final exigir invocação de métodos locais, é improvável que LINQ to SQL seja capaz de obrigar. As chamadas de método que não têm uma tradução conhecida no SQL não podem ser usadas como parte da consulta. Uma exceção a essa regra são chamadas de método que não têm argumentos dependentes de variáveis de consulta. Eles não são considerados parte da consulta traduzida e, em vez disso, são tratados como parâmetros.

Projeções ainda elaboradas (transformações) podem exigir a implementação da lógica de procedimento local. Para usar seus próprios métodos locais em uma projeção final, você precisará projetar duas vezes. A primeira projeção extrai todos os valores de dados que você precisará referenciar e a segunda projeção executa a transformação. Entre essas duas projeções está uma chamada para o operador AsEnumerable() que desloca o processamento nesse ponto de uma consulta LINQ to SQL para uma executada localmente.

C#

var q =
   from c in db.Customers
   where c.City == "London"
   select new { c.ContactName, c.Phone };
var q2 =
   from c in q.AsEnumerable()
   select new MyType {
      Name = DoNameProcessing(c.ContactName),
      Phone = DoPhoneProcessing(c.Phone)
   };

Visual Basic

Dim londonCustomers = From cust In db.Customer _
                      Where cust.City = "London" _
                      Select cust.ContactName, cust.Phone

Dim processedCustomers = From cust In londonCustomers.AsEnumerable() _
                         Select Contact = New With { _
                         .Name = DoNameProcessing(cust.ContactName), _
                         .Phone = DoPhoneProcessing(cust.Phone)}

Nota O operador AsEnumerable(), ao contrário de ToList() e ToArray(), não causa a execução da consulta. Ainda é adiado. O operador AsEnumerable() simplesmente altera a digitação estática da consulta, transformando um IQueryable<T> (IQueryable (ofT) no Visual Basic) em um IEnumerable<T> (IEnumerable (ofT) no Visual Basic), enganando o compilador para tratar o restante da consulta como executado localmente.

Consultas compiladas

É comum em muitos aplicativos executar consultas estruturalmente semelhantes muitas vezes. Nesses casos, é possível aumentar o desempenho compilando a consulta uma vez e executando-a várias vezes no aplicativo com parâmetros diferentes. Esse resultado é obtido em LINQ to SQL usando a classe CompiledQuery. O código a seguir mostra como definir uma consulta compilada:

C#

static class Queries
{
   public static Func<Northwind, string, IQueryable<Customer>>
      CustomersByCity = CompiledQuery.Compile((Northwind db, string city) =>
         from c in db.Customers where c.City == city select c);
}

Visual Basic

Class Queries
   public Shared Function(Of Northwind, String, IQueryable(Of Customer)) _      CustomersByCity = CompiledQuery.Compile( _
                Function(db As Northwind, city As String) _
                From cust In db.Customers Where cust.City = city)
End Class

O método Compile retorna um delegado que pode ser armazenado em cache e executado posteriormente várias vezes apenas alterando os parâmetros de entrada. O código a seguir mostra um exemplo disso:

C#

public IEnumerable<Customer> GetCustomersByCity(string city) {
         Northwind db = new Northwind();
         return Queries.CustomersByCity(myDb, city);
}

Visual Basic

Public Function GetCustomersByCity(city As String) _ 
               As IEnumerable(Of Customer)
         Dim db As Northwind = New Northwind()
         Return Queries.CustomersByCity(myDb, city)
End Function

Tradução de SQL

LINQ to SQL não executa consultas de fato; o banco de dados relacional executa. LINQ to SQL converte as consultas que você escreveu em consultas SQL equivalentes e as envia ao servidor para processamento. Como a execução é adiada, LINQ to SQL é capaz de examinar toda a consulta, mesmo que seja montada a partir de várias partes.

Como o servidor de banco de dados relacional não está realmente executando IL (além da integração CLR no SQL Server 2005); as consultas não são transmitidas para o servidor como IL. Na verdade, elas são transmitidas como consultas SQL parametrizadas em formato de texto.

É claro que o SQL , mesmo t-SQL com integração clr - é incapaz de executar a variedade de métodos que estão disponíveis localmente para o seu programa. Portanto, as consultas que você escreve devem ser convertidas em operações e funções equivalentes que estão disponíveis dentro do ambiente SQL.

A maioria dos métodos e operadores em tipos internos do .Net Framework tem traduções diretas no SQL. Alguns podem ser produzidos a partir das funções disponíveis. Os que não podem ser convertidos não são permitidos, gerando exceções em tempo de execução se você tentar usá-las. Há uma seção posterior no documento que detalha os métodos de estrutura que são implementados para traduzir para o SQL.

O ciclo de vida da entidade

LINQ to SQL é mais do que apenas uma implementação dos operadores de consulta padrão para bancos de dados relacionais. Além de traduzir consultas, é um serviço que gerencia seus objetos durante todo o tempo de vida, auxiliando você a manter a integridade de seus dados e automatizando o processo de traduzir suas modificações de volta para o repositório.

Em um cenário típico, os objetos são recuperados por meio de uma ou mais consultas e manipulados de alguma forma ou de outra até que o aplicativo esteja pronto para enviar as alterações de volta para o servidor. Esse processo pode se repetir várias vezes até que o aplicativo não tenha mais uso para essas informações. Nesse ponto, os objetos são recuperados pelo runtime, assim como objetos normais. Os dados, no entanto, permanecem no banco de dados. Mesmo depois de serem apagados de sua existência em tempo de execução, objetos que representam os mesmos dados ainda podem ser recuperados. Nesse sentido, o verdadeiro tempo de vida do objeto existe além de qualquer manifestação em tempo de execução única.

O foco deste capítulo é o ciclo de vida da entidade em que um ciclo se refere ao período de tempo de uma única manifestação de um objeto de entidade em um contexto de tempo de execução específico. O ciclo começa quando o DataContext se torna ciente de uma nova instância e termina quando o objeto ou DataContext não é mais necessário.

Controle de alterações

Depois que as entidades forem recuperadas do banco de dados, você estará livre para manipulá-las como desejar. Eles são seus objetos; use-os como quiser. Ao fazer isso, LINQ to SQL controla as alterações para que elas possam persistê-las no banco de dados quando SubmitChanges() for chamado.

LINQ to SQL começa a rastrear suas entidades no momento em que elas são recuperadas do banco de dados, antes que você coloque as mãos sobre elas. De fato, o serviço de gerenciamento de identidades discutido anteriormente já começou também. O controle de alterações custa muito pouco em sobrecarga adicional até que você realmente comece a fazer alterações.

C#

Customer cust = db.Customers.Single(c => c.CustomerID == "ALFKI");
cust.CompanyName = "Dr. Frogg's Croakers";

Visual Basic

' Query for a specific customer
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _ 
                      Where cust.CustomerID = id).First
targetCustomer.CompanyName = "Dr. Frogg's Croakers"

Assim que CompanyName é atribuído no exemplo acima, LINQ to SQL fica ciente da alteração e é capaz de registrá-la. Os valores originais de todos os membros de dados são retidos pelo serviço de controle de alterações.

O serviço de controle de alterações também registra todas as manipulações de propriedades de relação. Você usa propriedades de relação para estabelecer os vínculos entre suas entidades, mesmo que elas possam estar vinculadas por valores de chave no banco de dados. Não é necessário modificar diretamente os membros associados às colunas de chave. LINQ to SQL sincroniza automaticamente para você antes que as alterações sejam enviadas.

C#

Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
foreach (Order o in db.Orders.Where(o => o.CustomerID == custId2)) {
   o.Customer = cust1;
}

Visual Basic

Dim targetCustomer = (From cust In db.Customers _ 
                      Where cust.CustomerID = custId1).First

For Each ord In (From o In db.Orders _ 
                 Where o.CustomerID = custId2) 
   o.Customer = targetCustomer
Next

Você pode mover pedidos de um cliente para outro simplesmente fazendo uma atribuição para sua propriedade Customer . Como a relação existe entre o cliente e o pedido, você pode alterar a relação modificando ambos os lados. Você poderia ter removido-os facilmente da coleção Orders de cust2 e adicionado-os à coleção orders de cust1, conforme mostrado abaixo.

C#

Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
Customer cust2 = db.Customers.Single(c => c.CustomerID == custId2); 
// Pick some order
Order o = cust2.Orders[0]; 
// Remove from one, add to the other
cust2.Orders.Remove(o);
cust1.Orders.Add(o);
// Displays 'true'
Console.WriteLine(o.Customer == cust1);

Visual Basic

Dim targetCustomer1 = (From cust In db.Customers _ 
                       Where cust.CustomerID = custId1).First
Dim targetCustomer2 = (From cust In db.Customers _ 
                       Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer2.Orders(0) 
' Remove from one, add to the other
targetCustomer2.Orders.Remove(o)
targetCustomer1.Orders.Add(o)
' Displays 'True'
MsgBox(o.Customer = targetCustomer1)

É claro que, se você atribuir a uma relação o valor de nulo, você está de fato se livrando completamente da relação. Atribuir uma propriedade Customer de um pedido a nulo realmente remove o pedido da lista do cliente.

C#

Customer cust = db.Customers.Single(c => c.CustomerID == custId1); 
// Pick some order
Order o = cust.Orders[0];
// Assign null value
o.Customer = null;
// Displays 'false'
Console.WriteLine(cust.Orders.Contains(o));

Visual Basic

Dim targetCustomer = (From cust In db.Customers _ 
                       Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer.Orders(0)
' Assign null value
o.Customer = Nothing
' Displays 'False'
Msgbox(targetCustomer.Orders.Contains(o))

A atualização automática de ambos os lados de uma relação é essencial para manter a consistência do grafo de objeto. Ao contrário dos objetos normais, as relações entre os dados geralmente são bidirecionais. LINQ to SQL permite que você use propriedades para representar relações. No entanto, ele não oferece um serviço para manter automaticamente essas propriedades bidirecionais em sincronia. Esse é um nível de serviço que deve ser inserido diretamente em suas definições de classe. As classes de entidade geradas usando a ferramenta de geração de código têm essa funcionalidade. No próximo capítulo, mostraremos como fazer isso com suas próprias classes manuscritas.

No entanto, é importante observar que a remoção de uma relação não implica que um objeto tenha sido excluído do banco de dados. Lembre-se de que o tempo de vida dos dados subjacentes persiste no banco de dados até que a linha seja excluída da tabela. A única maneira de realmente excluir um objeto é removê-lo de sua coleção Table .

C#

Customer cust = db.Customers.Single(c => c.CustomerID == custId1); 
// Pick some order
Order o = cust.Orders[0];
// Remove it directly from the table (I want it gone!)
db.Orders.Remove(o);
// Displays 'false'.. gone from customer's Orders
Console.WriteLine(cust.Orders.Contains(o));
// Displays 'true'.. order is detached from its customer
Console.WriteLine(o.Customer == null);

Visual Basic

Dim targetCustomer = (From cust In db.Customers _ 
                          Where cust.CustomerID = custId1).First
' Pick some order
Dim o As Order = targetCustomer.Orders(0)
' Remove it directly from the table (I want it gone!)
db.Orders.Remove(o)
' Displays 'False'.. gone from customer’s Orders
Msgbox(targetCustomer.Orders.Contains(o))
' Displays 'True'.. order is detached from its customer
Msgbox(o.Customer = Nothing)

Assim como acontece com todas as outras alterações, a ordem não foi realmente excluída. Ele só parece assim para nós desde que foi removido e desanexado do resto de nossos objetos. Quando o objeto order foi removido da tabela Orders , ele foi marcado para exclusão pelo serviço de controle de alterações. A exclusão de fato do banco de dados ocorrerá quando as alterações forem enviadas em uma chamada para SubmitChanges(). Observe que o objeto em si nunca é excluído. O runtime gerencia o tempo de vida das instâncias de objeto, portanto, ele fica ao redor desde que você ainda esteja mantendo uma referência a ela. No entanto, depois que um objeto tiver sido removido de sua Tabela e as alterações enviadas, ele não será mais controlado pelo serviço de controle de alterações.

A única outra vez que uma entidade é deixada sem rastreamento é quando ela existe antes que o DataContext esteja ciente dela. Isso acontece sempre que você cria novos objetos em seu código. Você está livre para usar instâncias de classes de entidade em seu aplicativo sem nunca recuperá-las de um banco de dados. A abordagem de alterações e o gerenciamento de identidades se aplicam somente aos objetos dos quais o DataContext está ciente. Portanto, nenhum serviço está habilitado para instâncias recém-criadas até que você as adicione ao DataContext.

Isso pode ocorrer de duas maneiras. Você pode chamar o método Add() na coleção Table relacionada manualmente.

C#

Customer cust =
   new Customer {
      CustomerID = "ABCDE",
      ContactName = "Frond Smooty",
      CompanyTitle = "Eggbert's Eduware",
      Phone = "888-925-6000"
   };
// Add new customer to Customers table
db.Customers.Add(cust);

Visual Basic

Dim targetCustomer = New Customer With { _
         .CustomerID = “ABCDE”, _
         .ContactName = “Frond Smooty”, _
         .CompanyTitle = “Eggbert’s Eduware”, _
         .Phone = “888-925-6000”}
' Add new customer to Customers table
db.Customers.Add(cust)

Como alternativa, você pode anexar uma nova instância a um objeto do qual o DataContext já está ciente.

C#

// Add an order to a customer's Orders
cust.Orders.Add(
   new Order { OrderDate = DateTime.Now }
); 

Visual Basic

' Add an order to a customer's Orders
targetCustomer.Orders.Add( _
   New Order With { .OrderDate = DateTime.Now } )

O DataContext descobrirá suas novas instâncias de objeto mesmo se elas estiverem anexadas a outras novas instâncias.

C#

// Add an order and details to a customer's Orders
Cust.Orders.Add(
   new Order {
      OrderDate = DateTime.Now,
      OrderDetails = {
         new OrderDetail {
            Quantity = 1,
            UnitPrice = 1.25M,
            Product = someProduct
         }
      }
   }
); 

Visual Basic

' Add an order and details to a customer's Orders
targetCustomer.Orders.Add( _
   New Order With { _
      .OrderDate = DateTime.Now, _
      .OrderDetails = New OrderDetail With { _
               .Quantity = 1,
               .UnitPrice = 1.25M,
               .Product = someProduct 
      }
   } )

Basicamente, o DataContext reconhecerá qualquer entidade no grafo de objeto que não esteja atualmente rastreada como uma nova instância, se você chamou ou não o método Add().

Usando um DataContext somente leitura

Muitos cenários não exigem a atualização das entidades recuperadas do banco de dados. Mostrar uma tabela de Clientes em uma página da Web é um exemplo óbvio. Em todos esses casos, é possível melhorar o desempenho instruindo o DataContext a não controlar as alterações nas entidades. Isso é obtido especificando a propriedade ObjectTracking no DataContext como false como no seguinte código:

C#

      db.ObjectTracking = false;
      
      var q = db.Customers.Where( c => c.City = "London");
      foreach(Customer c in q)
         Display(c);

Visual Basic

db.ObjectTracking = False
      
      Dim londonCustomers = From cust In db.Customer _
                      Where cust.City = "London"
      For Each c in londonCustomers
         Display(c)
Next

Enviando alterações

Independentemente de quantas alterações você fizer em seus objetos, essas alterações só foram feitas em réplicas na memória. Nada ainda aconteceu com os dados reais no banco de dados. A transmissão dessas informações para o servidor não ocorrerá até que você as solicite explicitamente chamando SubmitChanges() no DataContext.

C#

Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// make changes here
db.SubmitChanges();

Visual Basic

Dim db As New Northwind("c:\northwind\northwnd.mdf")
' make changes here
db.SubmitChanges()

Quando você chamar SubmitChanges(), o DataContext tentará converter todas as suas alterações em comandos SQL equivalentes, inserindo, atualizando ou excluindo linhas em tabelas correspondentes. Essas ações podem ser substituídas por sua própria lógica personalizada se desejar, no entanto, a ordem de envio é orquestrada por um serviço do DataContext conhecido como processador de alterações.

A primeira coisa que acontece quando você chama SubmitChanges() é que o conjunto de objetos conhecidos é examinado para determinar se novas instâncias foram anexadas a elas. Essas novas instâncias são adicionadas ao conjunto de objetos rastreados. Em seguida, todos os objetos com alterações pendentes são ordenados em uma sequência de objetos com base nas dependências entre eles. Esses objetos cujas alterações dependem de outros objetos são sequenciados após suas dependências. Restrições de chave estrangeira e restrições de exclusividade no banco de dados desempenham um papel importante na determinação da ordenação correta de alterações. Em seguida, pouco antes de qualquer alteração real ser transmitida, uma transação é iniciada para encapsular a série de comandos individuais, a menos que já esteja no escopo. Por fim, uma a uma, as alterações nos objetos são convertidas em comandos SQL e enviadas para o servidor.

Neste ponto, todos os erros detectados pelo banco de dados farão com que o processo de envio seja anulado e uma exceção será gerada. Todas as alterações no banco de dados serão revertidas como se nenhum dos envios tivesse ocorrido. O DataContext ainda terá uma gravação completa de todas as alterações, portanto, é possível tentar corrigir o problema e reenvia-los chamando SubmitChanges() novamente.

C#

Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// make changes here 
try {
   db.SubmitChanges();
}
catch (Exception e) {
   // make some adjustments
   ...
   // try again
   db.SubmitChanges();
}

Visual Basic

Dim db As New Northwind("c:\northwind\northwnd.mdf")
' make changes here 
Try 
   db.SubmitChanges()
Catch e As Exception
   ' make some adjustments
   ...
   ' try again
   db.SubmitChanges()
End Try

Quando a transação em torno do envio for concluída com êxito, o DataContext aceitará as alterações nos objetos simplesmente esquecendo as informações de controle de alterações.

Alterações simultâneas

Há uma variedade de motivos pelos quais uma chamada para SubmitChanges() pode falhar. Você pode ter criado um objeto com uma chave primária inválida; um que já está em uso ou com um valor que viola alguma restrição marcar do banco de dados. Esses tipos de verificações são difíceis de inserir na lógica de negócios, pois geralmente exigem conhecimento absoluto de todo o estado do banco de dados. No entanto, o motivo mais provável para a falha é simplesmente que outra pessoa fez alterações nos objetos antes de você.

Certamente, isso seria impossível se você estivesse bloqueando cada objeto no banco de dados e usando uma transação totalmente serializada. No entanto, esse estilo de programação (simultaneidade pessimista) raramente é usado, pois é caro e conflitos verdadeiros raramente ocorrem. A forma mais popular de gerenciar alterações simultâneas é empregar uma forma de simultaneidade otimista. Nesse modelo, nenhum bloqueio nas linhas do banco de dados é feito. Isso significa que qualquer número de alterações no banco de dados pode ter ocorrido entre a hora em que você recuperou seus objetos pela primeira vez e a hora em que enviou suas alterações.

Portanto, a menos que você queira usar uma política que a última atualização ganhe, eliminando qualquer outra coisa que tenha ocorrido antes de você, você provavelmente deseja ser alertado sobre o fato de que os dados subjacentes foram alterados por outra pessoa.

O DataContext tem suporte interno para simultaneidade otimista detectando automaticamente conflitos de alteração. As atualizações individuais só serão bem-sucedidas se o estado atual do banco de dados corresponder ao estado em que você entendeu os dados quando recuperou seus objetos pela primeira vez. Isso acontece de acordo com o objeto, alertando você apenas para violações se ocorrerem em objetos nos quais você fez alterações.

Você pode controlar o grau em que o DataContext detecta conflitos de alteração ao definir suas classes de entidade. Cada atributo Column tem uma propriedade chamada UpdateCheck que pode ser atribuída a um dos três valores: Always, Never e WhenChanged. Se não definir o padrão para um atributo Column é Always, o que significa que os valores de dados representados por esse membro são sempre verificados quanto a conflitos, ou seja, a menos que haja um desempate óbvio como um carimbo de versão. Um atributo Column tem uma propriedade IsVersion que permite especificar se o valor dos dados constitui um carimbo de versão mantido pelo banco de dados. Se houver uma versão, a versão será usada sozinha para determinar se ocorreu um conflito.

Quando ocorrer um conflito de alteração, uma exceção será gerada como se fosse qualquer outro erro. A transação em torno do envio será anulada, mas o DataContext permanecerá o mesmo, permitindo a você a oportunidade de corrigir o problema e tentar novamente.

C#

while (retries < maxRetries) {
   Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");

   // fetch objects and make changes here

   try {
      db.SubmitChanges();
      break;
   }
   catch (ChangeConflictException e) {
      retries++;
   }
}

Visual Basic

Do While retries < maxRetries
   Dim db As New Northwind("c:\northwind\northwnd.mdf")

   ' fetch objects and make changes here

   Try
      db.SubmitChanges()
      Exit Do
   
   catch cce As ChangeConflictException
      retries += 1
   End Try
Loop

Se você estiver fazendo alterações em uma camada intermediária ou servidor, a coisa mais fácil que você pode fazer para corrigir um conflito de alterações é simplesmente recomeçar e tentar novamente, recriando o contexto e reaplicando as alterações. Opções adicionais são descritas na seção a seguir.

Transactions

Uma transação é um serviço fornecido por bancos de dados ou qualquer outro gerenciador de recursos que possa ser usado para garantir que uma série de ações individuais ocorram automaticamente; o que significa que todos eles têm êxito ou todos eles não. Se não o fizerem, eles também serão desfeitos automaticamente antes que qualquer outra coisa possa acontecer. Se nenhuma transação já estiver no escopo, o DataContext iniciará automaticamente uma transação de banco de dados para proteger as atualizações quando você chamar SubmitChanges().

Você pode optar por controlar o tipo de transação usada, seu nível de isolamento ou o que ela realmente engloba iniciando-a por conta própria. O isolamento de transação que o DataContext usará é conhecido como ReadCommitted.

C#

Product prod = db.Products.Single(p => p.ProductID == 15);

if (prod.UnitsInStock > 0)
   prod.UnitsInStock--;

using(TransactionScope ts = new TransactionScope()) {
   db.SubmitChanges();
   ts.Complete();
}

Visual Basic

Dim product = (From prod In db.Products _ 
                         Where prod.ProductID = 15).First

If product.UnitsInStock > 0) Then
   product.UnitsInStock -= 1
End If

Using ts As TransactionScope = New TransactionScope())
   db.SubmitChanges()
   ts.Complete()
End Using

O exemplo acima inicia uma transação totalmente serializada criando um novo objeto de escopo de transação. Todos os comandos de banco de dados executados no escopo da transação serão protegidos pela transação.

C#

Product prod = db.Products.Single(p => p.ProductId == 15);

if (prod.UnitsInStock > 0)
   prod.UnitsInStock--;

using(TransactionScope ts = new TransactionScope()) {
   db.ExecuteCommand("exec sp_BeforeSubmit");
   db.SubmitChanges();
   ts.Complete();
}

Visual Basic

Dim product = (From prod In db.Products _ 
                         Where prod.ProductID = 15).First

If product.UnitsInStock > 0) Then
   product.UnitsInStock -= 1
End If

Using ts As TransactionScope = New TransactionScope())
   db.ExecuteCommand(“exec sp_BeforeSubmit”)
   db.SubmitChanges()
   ts.Complete()
End Using

Essa versão modificada do mesmo exemplo usa o método ExecuteCommand() no DataContext para executar um procedimento armazenado no banco de dados logo antes das alterações serem enviadas. Independentemente do que o procedimento armazenado faz com o banco de dados, podemos ter certeza de que suas ações fazem parte da mesma transação.

Se a transação for concluída com êxito, o DataContext descartará todas as informações de acompanhamento acumuladas e tratará os novos estados das entidades como inalterados. No entanto, ele não reverterá as alterações em seus objetos se a transação falhar. Isso permite a flexibilidade máxima para lidar com problemas durante o envio de alterações.

Também é possível usar uma transação SQL local em vez do novo TransactionScope. LINQ to SQL oferece essa funcionalidade para ajudá-lo a integrar recursos LINQ to SQL em aplicativos de ADO.NET pré-existentes. No entanto, se você seguir esse caminho, precisará ser responsável por muito mais.

C#

Product prod = q.Single(p => p.ProductId == 15);

if (prod.UnitsInStock > 0)
   prod.UnitsInStock--;

db.Transaction = db.Connection.BeginTransaction();
try {
   db.SubmitChanges();
   db.Transaction.Commit();
}
catch {
   db.Transaction.Rollback();
   throw;
}
finally {
   db.Transaction = null;
}

Visual Basic

Dim product = (From prod In db.Products _ 
                         Where prod.ProductID = 15).First

If product.UnitsInStock > 0) Then
   product.UnitsInStock -= 1
End If

db.Transaction = db.Connection.BeginTransaction()
Try
   db.SubmitChanges()
   db.Transaction.Commit()

catch e As Exception
   db.Transaction.Rollback()
   Throw e
Finally
   db.Transaction = Nothing
End Try

Como você pode ver, o uso de uma transação de banco de dados controlada manualmente está um pouco mais envolvido. Você não só precisa iniciá-lo por conta própria, como também precisa informar o DataContext explicitamente para usá-lo atribuindo-o à propriedade Transaction . Em seguida, você deve usar um bloco try-catch para envolver sua lógica de envio, lembrando de informar explicitamente a transação para confirmar e informar explicitamente ao DataContext para aceitar alterações ou anular as transações se houver falha em qualquer ponto. Além disso, não se esqueça de definir a propriedade Transaction de volta como nula quando terminar.

Procedimentos armazenados

Quando SubmitChanges() é chamado, LINQ to SQL gera e executa comandos SQL para inserir, atualizar e excluir linhas no banco de dados. Essas ações podem ser substituídas por desenvolvedores de aplicativos e, em seu local, o código personalizado pode ser usado para executar as ações desejadas. Dessa forma, instalações alternativas, como procedimentos armazenados no banco de dados, podem ser invocadas automaticamente pelo processador de alterações.

Considere um procedimento armazenado para atualizar as unidades em estoque para a tabela Products no banco de dados de exemplo Northwind. A declaração SQL do procedimento é a seguinte.

SQL

create proc UpdateProductStock
   @id               int,
   @originalUnits    int,
   @decrement         int
as

Você pode usar o procedimento armazenado em vez do comando normal de atualização gerada automaticamente definindo um método em seu DataContext fortemente tipado. Mesmo que a classe DataContext esteja sendo gerada automaticamente pela ferramenta de geração de código LINQ to SQL, você ainda poderá especificar esses métodos em uma classe parcial própria.

C#

public partial class Northwind : DataContext
{
   ...

   public void UpdateProduct(Product original, Product current) {
      // Execute the stored procedure for UnitsInStock update
      if (original.UnitsInStock != current.UnitsInStock) {
         int rowCount = this.ExecuteCommand(
            "exec UpdateProductStock " +
            "@id={0}, @originalUnits={1}, @decrement={2}",
            original.ProductID,
            original.UnitsInStock,
            (original.UnitsInStock - current.UnitsInStock)
         );
         if (rowCount < 1)
            throw new Exception("Error updating");
      }
      ...
   }
}

Visual Basic

Partial Public Class Northwind
         Inherits DataContext

   ...

   Public Sub UpdateProduct(original As Product, current As Product)
      ‘ Execute the stored procedure for UnitsInStock update
      If original.UnitsInStock <> current.UnitsInStock Then
         Dim rowCount As Integer = ExecuteCommand( _
            "exec UpdateProductStock " & _
            "@id={0}, @originalUnits={1}, @decrement={2}", _
            original.ProductID, _
            original.UnitsInStock, _
            (original.UnitsInStock - current.UnitsInStock) )
         If rowCount < 1 Then
            Throw New Exception(“Error updating”)
         End If
      End If
      ...
   End Sub
End Class

A assinatura do método e do parâmetro genérico instrui o DataContext a usar esse método no lugar de uma instrução de atualização gerada. Os parâmetros originais e atuais são usados por LINQ to SQL para passar as cópias originais e atuais do objeto do tipo especificado. Os dois parâmetros estão disponíveis para detecção de conflito de simultaneidade otimista.

Nota Se você substituir a lógica de atualização padrão, a detecção de conflitos será sua responsabilidade.

O procedimento armazenado UpdateProductStock é invocado usando o método ExecuteCommand() do DataContext. Ele retorna o número de linhas afetadas e tem a seguinte assinatura:

C#

public int ExecuteCommand(string command, params object[] parameters);

Visual Basic

Public Function ExecuteCommand(command As String, _
         ParamArray parameters() As Object) As Integer

A matriz de objetos é usada para passar parâmetros necessários para executar o comando.

Semelhante ao método de atualização, os métodos de inserção e exclusão podem ser especificados. Os métodos inserir e excluir levam apenas um parâmetro do tipo de entidade a ser atualizado. Por exemplo, métodos para inserir e excluir uma instância de Produto podem ser especificados da seguinte maneira:

C#

public void InsertProduct(Product prod) { ... }
public void DeleteProudct(Product prod) { ... }

Visual Basic

Public Sub InsertProduct(prod As Product)  ... 
Public Sub DeleteProudct(prod As Product)  ... 

Classes de entidade In-Depth

Usando atributos

Uma classe de entidade é como qualquer classe de objeto normal que você pode definir como parte do seu aplicativo, exceto que ela é anotada com informações especiais que a associam a uma tabela de banco de dados específica. Essas anotações são feitas como atributos personalizados na declaração de classe. Os atributos só são significativos quando você usa a classe em conjunto com LINQ to SQL. Eles são semelhantes aos atributos de serialização XML no .NET Framework. Esses atributos de "dados" fornecem LINQ to SQL informações suficientes para converter consultas de seus objetos em consultas SQL no banco de dados e alterações em seus objetos em comandos de inserção, atualização e exclusão do SQL.

Também é possível representar as informações de mapeamento usando um arquivo de mapeamento XML em vez de atributos. Esse cenário é descrito mais detalhadamente na seção Mapeamento Externo.

Atributo de banco de dados

O atributo Database será usado para especificar o nome padrão do banco de dados se ele não for fornecido pela conexão. Os atributos de banco de dados podem ser aplicados a declarações DataContext fortemente tipada. Esse atributo é opcional.

Atributo de banco de dados

Propriedade Type Descrição
Nome String Especifica o nome do banco de dados. As informações serão usadas somente se a conexão em si não especificar o nome do banco de dados. Se esse atributo Database não existir na declaração de contexto e um não for especificado pela conexão, supõe-se que o banco de dados tenha o mesmo nome que a classe de contexto.

C#

[Database(Name="Database#5")]
public class Database5 : DataContext {
   ...
}

Visual Basic

<Database(Name:="Database#5")> _
Public Class Database5 
               Inherits DataContext
   ...
End Class

Atributo table

O atributo Table é usado para designar uma classe como uma classe de entidade associada a uma tabela de banco de dados. Classes com o atributo Table serão tratadas especialmente por LINQ to SQL.

Atributo table

Propriedade Type Descrição
Nome String Especifica o nome da tabela. Se essas informações não forem especificadas, supõe-se que a tabela tenha o mesmo nome que a classe de entidade.

C#

[Table(Name="Customers")]
public class Customer {
   ...
}

Visual Basic

<Table(Name:="Customers")> _
Public Class Customer 
   ...
End Class

Atributo da coluna

O atributo Column é usado para designar um membro de uma classe de entidade que representa uma coluna em uma tabela de banco de dados. Ela pode ser aplicada a qualquer campo ou propriedade, pública, privada ou interna. Somente membros identificados como colunas são persistidos quando LINQ to SQL salva alterações no banco de dados.

Atributo de Coluna

Propriedade Type Descrição
Nome String O nome da coluna na tabela ou exibição. Se não for especificado, presume-se que a coluna tenha o mesmo nome que o membro da classe.
Armazenamento String O nome do armazenamento subjacente. Se especificado, ele informa LINQ to SQL como ignorar o acessador de propriedade pública para o membro de dados e interagir com o próprio valor bruto. Se não for especificado LINQ to SQL obtém e define o valor usando o acessador público.
Dbtype String O tipo de coluna de banco de dados especificada usando modificadores e tipos de banco de dados. Esse será o texto exato usado para definir a coluna em um comando de declaração de tabela T-SQL. Se não for especificado, o tipo de coluna de banco de dados será inferido do tipo de membro. O tipo de banco de dados específico só será necessário se o método CreateDatabase() for usado para criar uma instância do banco de dados.
IsPrimaryKey Bool Se definido como true, o membro de classe representará uma coluna que faz parte da chave primária da tabela. Se mais de um membro da classe for designado como a ID, a chave primária será considerada uma composição das colunas associadas.
Isdbgenerated Boolean Identifica que o valor da coluna do membro é gerado automaticamente pelo banco de dados. As chaves primárias designadas IsDbGenerated=true também devem ter um DBType com o modificador IDENTITY . Isdbgenerated os membros são sincronizados imediatamente após a inserção da linha de dados e ficam disponíveis após a conclusão de SubmitChanges().
Isversion Boolean Identifica o tipo de coluna do membro como um carimbo de data/hora do banco de dados ou um número de versão. Os números de versão são incrementados e as colunas de carimbo de data/hora são atualizadas pelo banco de dados sempre que a linha associada é atualizada. Os membros com IsVersion=true são sincronizados imediatamente após a atualização da linha de dados. Os novos valores ficam visíveis após a conclusão de SubmitChanges().
UpdateCheck UpdateCheck Determina como LINQ to SQL implementa a detecção de conflitos de simultaneidade otimista. Se nenhum membro for designado como IsVersion=true , a detecção será feita comparando valores de membro originais com o estado atual do banco de dados. Você pode controlar quais membros LINQ to SQL usa durante a detecção de conflitos, dando a cada membro um valor de enumeração UpdateCheck.
  • Sempre: sempre use esta coluna para detecção de conflitos
  • Nunca: nunca use esta coluna para detecção de conflitos
  • WhenChanged: use esta coluna somente quando o membro tiver sido alterado pelo aplicativo
IsDiscriminator Boolean Determina se o membro da classe contém o valor discriminatório de uma hierarquia de herança.
Expression String Não afeta a operação de LINQ to SQL, mas é usado durante .CreateDatabase() como uma expressão SQL bruta que representa a expressão de coluna computada.
CanBeNull Boolean Indica que o valor pode conter o valor nulo. Isso geralmente é inferido do tipo CLR do membro da entidade. Use esse atributo para indicar que um valor de cadeia de caracteres é representado como uma coluna não anulável no banco de dados.
AutoSync AutoSync Especifica se a coluna é sincronizada automaticamente do valor gerado pelo banco de dados em comandos de inserção ou atualização. Os valores válidos para essa marca são OnInsert, Always e Never.

Uma classe de entidade típica usará atributos Column em propriedades públicas e armazenará valores reais em campos privados.

C#

private string _city;

[Column(Storage="_city", DBType="NVarChar(15)")]
public string City {
   get { ... }
   set { ... }
}

Visual Basic

Private _city As String

<Column(Storage:="_city", DBType:="NVarChar(15)")> _
public Property City As String
   Get
   set
End Property

O DBType só é especificado para que o método CreateDatabase() possa construir a tabela com o tipo mais preciso. Caso contrário, o conhecimento de que a coluna subjacente é limitada a 15 caracteres não é utilizado.

Os membros que representam a chave primária de um tipo de banco de dados geralmente serão associados a valores gerados automaticamente.

C#

private string _orderId;

[Column(Storage="_orderId", IsPrimaryKey=true, IsDbGenerated = true,
   DBType="int NOT NULL IDENTITY")]
public string OrderId {
   get { ... }
   set { ... }
}

Visual Basic

Private _orderId As String

<Column(Storage:="_orderId", IsPrimaryKey:=true, _
           IsDbGenerated:= true, DBType:="int NOT NULL IDENTITY")> _
public Property OrderId As String
   Get
   Set
End Property

Se você especificar o DBType, inclua o modificador IDENTITY . LINQ to SQL não aumentará um DBType especificado personalizado. No entanto, se o DBType for deixado não especificado LINQ to SQL inferirá que o modificador IDENTITY é necessário ao criar o Banco de Dados por meio do método CreateDatabase().

Da mesma forma, se a propriedade IsVersion for verdadeira, o DBType deverá especificar os modificadores corretos para designar um número de versão ou coluna de carimbo de data/hora. Se nenhum DBType for especificado, LINQ to SQL inferirá os modificadores corretos.

Você pode controlar o acesso a um membro associado a uma coluna gerada automaticamente, carimbo de versão ou qualquer coluna que queira ocultar designando o nível de acesso do membro ou até mesmo limitando o próprio acessador.

C#

private string _customerId;

[Column(Storage="_customerId", DBType="NCHAR(5) ")]
public string CustomerID {
   get { ... }
}

Visual Basic

Private _customerId As String

<Column(Storage:="_customerId", DBType:="NCHAR(5)")> _
Public Property CustomerID As String
   Get
End Property

A propriedade CustomerID do Pedido pode ser feita somente leitura por não definir um acessador definido. LINQ to SQL ainda pode obter e definir o valor subjacente por meio do membro de armazenamento.

Você também pode tornar um membro completamente inacessível para o restante do aplicativo colocando um atributo Column em um membro privado. Isso permite que a classe de entidade contenha informações relevantes para a lógica de negócios da classe sem expô-la em geral. Embora os membros privados façam parte dos dados traduzidos, uma vez que eles são privados, você não pode se referir a eles em uma consulta integrada à linguagem.

Por padrão, todos os membros são usados para executar a detecção de conflito de simultaneidade otimista. Você pode controlar se um membro específico é usado especificando seu valor UpdateCheck .

C#

[Column(Storage="_city", UpdateCheck=UpdateCheck.WhenChanged)]
public string City {
   get { ... }
   set { ... }
}

Visual Basic

<Column(Storage:="_city", UpdateCheck:=UpdateCheck.WhenChanged)> _
Public Property City As String
   Get
   Set
End Property

A tabela a seguir mostra os mapeamentos permitidos entre tipos de banco de dados e o tipo CLR correspondente. Use esta tabela como um guia ao determinar qual tipo CLR usar para representar uma coluna de banco de dados específica.

Tipo de banco de dados e mapeamentos permitidos de tipo CLR correspondentes

Tipo de Banco de Dados Tipo CLR do .NET Comentários
bit, tinyint, smallint, int, bigint Bye, Int16, Uint16, Int32, Uint32, Int64, Uint64 Conversões com perda possíveis. Os valores podem não ser arredondados.
bit Boolean  
decimal, numérico, smallmoney, money Decimal A diferença de escala pode resultar em conversão de perda. Talvez não seja uma viagem de ida e volta.
real, float Single e Double Diferenças de precisão.
char, varchar, text, nchar, nvarchar, ntext String Diferenças de localidade possíveis.
datetime, smalldatetime Datetime Precisão diferente pode causar problemas de conversão e ida e volta com perda.
UNIQUEIDENTIFIER Guid Regras de ordenação diferentes. A classificação pode não funcionar conforme o esperado.
timestamp Byte[] (Byte() no Visual Basic), Binary A matriz de bytes é tratada como um tipo escalar. O usuário é responsável por alocar armazenamento adequado quando o construtor é chamado. Ele é considerado imutável e não é controlado por alterações.
binary, varbinary Byte[] (Byte() no Visual Basic), Binary  

Atributo de Associação

O atributo Association é usado para designar uma propriedade que representa uma associação de banco de dados como uma relação de chave estrangeira para chave primária.

Atributo de Associação

Propriedade Type Descrição
Nome String O nome da associação. Isso geralmente é o mesmo que o nome da restrição de chave estrangeira do banco de dados. Ele é usado quando CreateDatabase() é usado para criar uma instância do banco de dados para gerar a restrição relevante. Ele também é usado para ajudar a distinguir entre várias relações em uma única classe de entidade que se refere à mesma classe de entidade de destino. Nesse caso, as propriedades de relação em lados da relação (se ambas estiverem definidas) devem ter o mesmo nome.
Armazenamento String O nome do membro de armazenamento subjacente. Se especificado, ele informará LINQ to SQL como ignorar o acessador de propriedade pública para o membro de dados e interagir com o próprio valor bruto. Se não for especificado LINQ to SQL obtém e define o valor usando o acessador público. É recomendável que todos os membros da associação sejam propriedades com membros de armazenamento separados identificados.
Thiskey String Uma lista separada por vírgulas de nomes de um ou mais membros dessa classe de entidade que representam os valores principais desse lado da associação. Se não for especificado, presume-se que os membros sejam os membros que compõem a chave primária.
Otherkey String Uma lista separada por vírgulas de nomes de um ou mais membros da classe de entidade de destino que representam os valores de chave do outro lado da associação. Se não for especificado, presume-se que os membros sejam os membros que compõem a chave primária da outra classe de entidade.
IsUnique Boolean True se houver uma restrição de exclusividade na chave estrangeira, indicando uma relação 1:1 verdadeira. Essa propriedade raramente é usada, pois as relações 1:1 são quase impossíveis de gerenciar no banco de dados. A maioria dos modelos de entidade são definidos usando relações 1:n mesmo quando são tratados como 1:1 por desenvolvedores de aplicativos.
IsForeignKey Boolean True se o tipo "outro" de destino da associação for o pai do tipo de origem. Com relações de chave estrangeira para chave primária, o lado que mantém a chave estrangeira é o filho e o lado que mantém a chave primária é o pai.
Deleterule String Usado para adicionar o comportamento de exclusão a essa associação. Por exemplo, "CASCADE" adicionaria "ON DELETE CASCADE" à relação FK. Se definido como nulo, nenhum comportamento de exclusão será adicionado.

As propriedades de associação representam uma única referência a outra instância de classe de entidade ou representam uma coleção de referências. As referências singleton devem ser codificadas na classe de entidade usando o tipo de valor EntityRef<T>(EntityRef (OfT) no Visual Basic) para armazenar a referência real. O tipo EntityRef é como LINQ to SQL habilita o carregamento adiado de referências.

C#

class Order
{
   ...
   private EntityRef<Customer> _Customer;

   [Association(Name="FK_Orders_Customers", Storage="_Customer",
      ThisKey="CustomerID")]
   public Customer Customer {
      get { return this._Customer.Entity; }
      set { this._Customer.Entity = value;
            // Additional code to manage changes }
   }
}

Visual Basic

Class Order

   ...
   Private _customer As EntityRef(Of Customer)

   <Association(Name:="FK_Orders_Customers", _
            Storage:="_Customer", ThisKey:="CustomerID")> _
   public Property Customer() As Customer
      Get  
         Return _customer.Entity
      End Get   
   Set (value As Customer)
      _customer.Entity = value
      ‘ Additional code to manage changes
   End Set
End Class

A propriedade pública é digitada como Customer, não EntityRef<Customer>. É importante não expor o tipo EntityRef como parte da API pública, pois as referências a esse tipo em uma consulta não serão convertidas em SQL.

Da mesma forma, uma propriedade de associação que representa uma coleção deve usar o tipo de coleção EntitySet<T> (EntitySet(OfT) no Visual Basic) para armazenar a relação.

C#

class Customer
{
   ...
   private EntitySet<Order> _Orders;

   [Association(Name="FK_Orders_Customers", Storage="_Orders",
      OtherKey="CustomerID")]
   public EntitySet<Order> Orders {
      get { return this._Orders; }
      set { this._Orders.Assign(value); }
   }
} 

Visual Basic

Class Customer

   ...
   Private _Orders As EntitySet(Of Order)

   <Association(Name:="FK_Orders_Customers", _
         Storage:="_Orders", OtherKey:="CustomerID")> _
   public Property Orders() As EntitySet(Of Order)
      Get
           Return _Orders
      End Get
   Set (value As EntitySet(Of Order))
      _Orders.Assign(value)
   End Property
End Class

No entanto, como um EntitySet<T> (EntitySet(OfT) no Visual Basic é uma coleção, é válido usar o EntitySet como o tipo de retorno. Também é válido disfarçar o tipo verdadeiro da coleção, usando a interface ICollection<T> (ICollection(OfT) no Visual Basic).

C#

class Customer
{
   ...

   private EntitySet<Order> _Orders;

   [Association(Name="FK_Orders_Customers", Storage="_Orders",
      OtherKey="CustomerID")]
   public ICollection<Order> Orders {
      get { return this._Orders; }
      set { this._Orders.Assign(value); }
   }
}

Visual Basic

Class Customer

   ...
   Private _orders As EntitySet(Of Order)

   <Association(Name:="FK_Orders_Customers", _
         Storage:="_Orders", OtherKey:="CustomerID")> _
   public Property Orders() As ICollection (Of Order)
      Get
           Return _orders
      End Get
Set (value As ICollection (Of Order))
         _orders.Assign(value)
      End Property
End Class

Certifique-se de usar o método Assign() no EntitySet se você expor um setter público para a propriedade. Isso permite que a classe de entidade continue usando a mesma instância de coleção, pois ela já pode estar vinculada ao serviço de controle de alterações.

Atributo ResultType

Esse atributo especifica um tipo de elemento de uma sequência enumerável que pode ser retornada de uma função que foi declarada para retornar a interface IMultipleResults . Esse atributo pode ser especificado mais de uma vez.

Atributo ResultType

Propriedade Type Descrição
Type Tipo Tipo dos resultados retornados.

Atributo StoredProcedure

O atributo StoredProcedure é usado para declarar que uma chamada para um método definido no tipo DataContext ou Schema é convertida como uma chamada para um procedimento armazenado de banco de dados.

Atributo StoredProcedure

Propriedade Type Descrição
Nome String O nome do procedimento armazenado no banco de dados. Se não for especificado, supõe-se que o procedimento armazenado tenha o mesmo nome que o método

Atributo de função

O atributo Function é usado para declarar que uma chamada para um método definido em um DataContext ou Schema é convertida como uma chamada para uma função escalar ou com valor de tabela definida pelo usuário do banco de dados.

Atributo de função

Propriedade Type Descrição
Nome String O nome da função no banco de dados. Se não for especificado, supõe-se que a função tenha o mesmo nome que o método

Atributo de parâmetro

O atributo Parameter é usado para declarar um mapeamento entre um método e os parâmetros de um procedimento armazenado de banco de dados ou função definida pelo usuário.

Atributo de parâmetro

Propriedade Type Descrição
Nome String O nome do parâmetro no banco de dados. Se não for especificado, o parâmetro será inferido do nome do parâmetro do método.
Dbtype String O tipo de parâmetro especificado usando modificadores e tipos de banco de dados.

Atributo InheritanceMapping

O atributo InheritanceMapping é usado para descrever a correspondência entre determinados códigos discriminatórios e um subtipo de herança. Todos os atributos InheritanceMapping usados para uma hierarquia de herança devem ser declarados no tipo raiz da hierarquia.

Atributo InheritanceMapping

Propety Type Descrição
Código Objeto O valor do código discriminatório.
Tipo Tipo O subtipo Herança. Pode ser qualquer tipo não abstrato na hierarquia de herança, incluindo o tipo raiz.
IsDefault Boolean Determina se o subtipo de herança especificado é o tipo padrão construído quando LINQ to SQL encontra um código discriminatório que não é definido pelos atributos InheritanceMapping. Exatamente um dos atributos InheritanceMapping deve ser declarado com IsDefault como true.

Consistência do grafo

Um grafo é um termo geral para uma estrutura de dados de objetos que se referem uns aos outros por referências. Uma hierarquia (ou árvore) é uma forma degenerada de grafo. Os modelos de objeto específicos do domínio geralmente descrevem uma rede de referências que são melhor descritas como um grafo de objetos. A integridade do grafo de objeto é de vital importância para a estabilidade do aplicativo. É por isso que é importante garantir que as referências no grafo permaneçam consistentes com suas regras de negócios e/ou restrições definidas no banco de dados.

LINQ to SQL não gerencia automaticamente a consistência das referências de relação para você. Quando as relações são bidirecionais, uma alteração para um lado da relação deve atualizar automaticamente a outra. Observe que é incomum que objetos normais se comportem dessa maneira, portanto, é improvável que você tenha projetado seus objetos dessa maneira de outra forma.

LINQ to SQL fornece alguns mecanismos para facilitar esse trabalho e um padrão a ser seguido para garantir que você está gerenciando suas referências corretamente. As classes de entidade geradas pela ferramenta de geração de código implementarão automaticamente os padrões corretos.

C#

public class Customer() {
   this._Orders =
      new EntitySet<Order>(
         new Action<Order>(this.attach_Orders),
         new Action<Order>(this.detach_Orders));
);}

Visual Basic

Public Class Customer()
         _Orders = New EntitySet(Of Order)( _
              New Action(Of Order)(attach_Orders), _
                 New Action(Of Order)(detach_Orders))
      End Class
);}

O tipo EntitySet<T> (EntitySet(OfT) no Visual Basic) tem um construtor que permite que você forneça dois delegados para serem usados como retornos de chamada; o primeiro quando um item é adicionado à coleção, o segundo quando ele é removido. Como você pode ver no exemplo, o código especificado para esses delegados pode e deve ser gravado para atualizar a propriedade de relação inversa. É assim que a propriedade Customer em uma instância de Pedido é alterada automaticamente quando um pedido é adicionado à coleção Orders de um cliente.

Implementar a relação do outro lado não é tão fácil. O EntityRef<T> (EntityRef(OfT) no Visual Basic) é um tipo de valor definido para conter o mínimo de sobrecarga adicional da referência de objeto real possível. Não tem espaço para um par de delegados. Em vez disso, o código que gerencia a consistência do grafo de referências singleton deve ser inserido nos próprios acessadores de propriedade.

C#

[Association(Name="FK_Orders_Customers", Storage="_Customer",
   ThisKey="CustomerID")]
public Customer Customer {
   get {
      return this._Customer.Entity;
   }
   set {
      Customer v = this._Customer.Entity;
      if (v != value) {
         if (v != null) {
            this._Customer.Entity = null;
            v.Orders.Remove(this);
         }
         this._Customer.Entity = value;
         if (value != null) {
            value.Orders.Add(this);
         }
      }
   }
}

Visual Basic

<Association(Name:="FK_Orders_Customers", _
         Storage:="_Customer", ThisKey:="CustomerID")> _
Public Property Customer As Customer 
   Get
      Return _Customer.Entity
   End Get
   Set (value As Customer)
      Dim cust As Customer v = _customer.Entity
      if cust IsNot value Then
         If cust IsNot Nothing Then
            _Customer.Entity = Nothing
            cust.Orders.Remove(Me)
         End If

         _customer.Entity = value
         if value IsNot Nothing Then
            value.Orders.Add(Me)
         End If
      End If
   End Set
End Property

Dê uma olhada no setter. Quando a propriedade Customer está sendo alterada, a instância do pedido é removida pela primeira vez da coleção pedidos do cliente atual e, depois, adicionada apenas mais tarde à coleção do novo cliente. Observe que antes da chamada para Remove() ser feita, a referência de entidade real é definida como nula. Isso é feito para evitar a recursão quando o método Remove() é chamado. Lembre-se de que o EntitySet usará delegados de retorno de chamada para atribuir a propriedade Customer desse objeto a nulo. A mesma coisa acontece logo antes da chamada para Add(). A referência de entidade real é atualizada para o novo valor. Isso reduzirá novamente qualquer possível recursão e, claro, realizará a tarefa do setter em primeiro lugar.

A definição de uma relação um-para-um é muito semelhante à definição de uma relação um-para-muitos do lado da referência singleton. Em vez de Add() e Remove() serem chamados, um novo objeto é atribuído ou um nulo é atribuído para cortar a relação.

Novamente, é vital que as propriedades de relação mantenham a consistência do grafo do objeto. Se o grafo de objeto na memória for inconsistente com os dados do banco de dados, uma exceção em tempo de execução será gerada quando o método SubmitChanges for chamado. Considere usar a ferramenta de geração de código para manter o trabalho de consistência para você.

Alterar Notificações

Seus objetos podem participar do processo de controle de alterações. Não é necessário que eles o façam, mas eles podem reduzir consideravelmente a quantidade de sobrecarga necessária para acompanhar possíveis alterações de objeto. É provável que seu aplicativo recupere muito mais objetos de consultas do que acabará sendo modificado. Sem ajuda proativa de seus objetos, o serviço de controle de alterações é limitado em como ele pode realmente controlar as alterações.

Como não há nenhum serviço de interceptação verdadeiro no runtime, o acompanhamento formal não ocorre de fato. Em vez disso, cópias duplicadas dos objetos são armazenadas quando são recuperadas pela primeira vez. Posteriormente, quando você chama SubmitChanges(), essas cópias são usadas para comparar com as que você recebeu. Se os valores forem diferentes, o objeto será modificado. Isso significa que cada objeto requer duas cópias na memória, mesmo que você nunca as altere.

Uma solução melhor é fazer com que os próprios objetos anunciem ao serviço de controle de alterações quando eles forem realmente alterados. Isso pode ser feito fazendo com que o objeto implemente uma interface que expõe um evento de retorno de chamada. Em seguida, o serviço de controle de alterações pode conectar cada objeto e receber notificações quando eles forem alterados.

C#

[Table(Name="Customers")]
public partial class Customer: INotifyPropertyChanging {

   public event PropertyChangingEventHandler PropertyChanging;

   private void OnPropertyChanging() {
      if (this.PropertyChanging != null) {
         this.PropertyChanging(this, emptyEventArgs);
      }
   }

   private string _CustomerID;

   [Column(Storage="_CustomerID", IsPrimaryKey=true)]
   public string CustomerID {
      get {
         return this._CustomerID;
      }
      set {
         if ((this._CustomerID != value)) {
            this.OnPropertyChanging("CustomerID");
            this._CustomerID = value;
         }
      }
   }
}

Visual Basic

<Table(Name:="Customers")> _
Partial Public Class Customer 
         Inherits INotifyPropertyChanging
Public Event PropertyChanging As PropertyChangingEventHandler _
        Implements INotifyPropertyChanging.PropertyChanging

   Private Sub OnPropertyChanging()
         RaiseEvent PropertyChanging(Me, emptyEventArgs)
   End Sub

   private _customerID As String 

   <Column(Storage:="_CustomerID", IsPrimaryKey:=True)>
   public Property CustomerID() As String
      Get
         Return_customerID
      End Get
      Set (value As Customer)
         If _customerID IsNot value Then
            OnPropertyChanging(“CustomerID”)
            _CustomerID = value
         End IF
      End Set
   End Function
End Class

Para ajudar no controle de alterações aprimorado, suas classes de entidade devem implementar a interface INotifyPropertyChanging . Ele só exige que você defina um evento chamado PropertyChanging— o serviço de controle de alterações e, em seguida, registra com seu evento quando seus objetos entram em sua posse. Tudo o que você precisa fazer é gerar esse evento imediatamente antes de alterar o valor de uma propriedade.

Não se esqueça de colocar a mesma lógica de geração de eventos em seus setters de propriedade de relação também. Para EntitySets, gere os eventos nos delegados fornecidos.

C#

public Customer() {
   this._Orders =
      new EntitySet<Order>(
         delegate(Order entity) {
            this.OnPropertyChanging("Orders");
            entity.Customer = this;
         },
         delegate(Order entity) {
            this.onPropertyChanging("Orders");
            entity.Customer = null;
         }
      );
}

Visual Basic

Dim _orders As EntitySet(Of Order)
Public Sub New()
   _orders = New EntitySet(Of Order)( _
      AddressOf OrderAdding, AddressOf OrderRemoving)
End Sub

Sub OrderAdding(ByVal o As Order)
   OnPropertyChanging()
   o.Customer = Me
End Sub

Sub OrderRemoving(ByVal o As Order)
   OnPropertyChanging()
   o.Customer = Nothing
End Sub

Herança

LINQ to SQL dá suporte ao mapeamento de tabela única, pelo qual uma hierarquia de herança inteira é armazenada em uma única tabela de banco de dados. A tabela contém a união mesclada de todas as colunas de dados possíveis para toda a hierarquia e cada linha tem nulos nas colunas que não são aplicáveis ao tipo da instância representada pela linha. A estratégia de mapeamento de tabela única é a representação mais simples de herança e fornece características de desempenho bom para várias categorias diferentes de consultas.

Mapeamento

Para implementar esse mapeamento usando LINQ to SQL, você precisa especificar os seguintes atributos e propriedades de atributo na classe raiz da hierarquia de herança:

  • O atributo [Tabela] (<Tabela> no Visual Basic).
  • Um atributo [InheritanceMapping] (<InheritanceMapping> no Visual Basic) para cada classe na estrutura de hierarquia. Para classes não abstratas, esse atributo deve definir uma propriedade Code (um valor que aparece na tabela de banco de dados na coluna Discriminatório de Herança para indicar a qual classe ou subclasse essa linha de dados pertence) e uma propriedade Type (que especifica qual classe ou subclasse o valor da chave significa).
  • Uma propriedade IsDefault em um único atributo [InheritanceMapping] (<InheritanceMapping> no Visual Basic). Essa propriedade serve para designar um mapeamento de "fallback" caso o valor discriminatório da tabela de banco de dados não corresponda a nenhum dos valores de Código nos mapeamentos de herança.
  • Uma propriedade IsDiscriminator para um atributo [Column] (<Column> in Visual Basic), para significar que essa é a coluna que contém o valor de Código para mapeamento de herança.

Qualquer atributo ou propriedade de especial são necessários nas subclasses. Observe especialmente que as subclasses não têm o atributo [Tabela] (<Tabela> no Visual Basic).

No exemplo a seguir, os dados contidos nas subclasses Carro e Caminhão são mapeados para a tabela de banco de dados individual Veículo. (Para simplificar o exemplo, o código de exemplo usa campos em vez de propriedades para mapeamento de coluna.)

C#

[Table]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
   IsDefault = true)]
public class Vehicle
{
   [Column(IsDiscriminator = true)]
   public string Key;
   [Column(IsPrimaryKey = true)]
   public string VIN;
   [Column]
   public string MfgPlant;
}
public class Car : Vehicle
{
   [Column]
   public int TrimCode;
   [Column]
   public string ModelName;
}

public class Truck : Vehicle
{
   [Column]
   public int Tonnage;
   [Column]
   public int Axles;
}

Visual Basic

<Table> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), _
              IsDefault:=true)> _
Public Class Vehicle

   <Column(IsDiscriminator:=True)> _
   Public Key As String
   <Column(IsPrimaryKey:=True)> _
   Public VIN As String
   <Column> _
   Public MfgPlant As String
End Class
Public Class Car
       Inherits Vehicle
   <Column> _
   Public TrimCode As Integer
   <Column> _
   Public ModelName As String
End Class

Public class Truck
       Inherits Vehicle 
   <Column> _
   public Tonnage As Integer
   <Column> _
   public Axles As Integer
End Class

O diagrama de classe aparece da seguinte maneira:

Figura 1. Diagrama de classe de veículo

Ao exibir o diagrama de banco de dados resultante no Explorer do servidor, você verá que todas as colunas foram mapeadas para uma única tabela, conforme mostrado aqui:

Figura 2. Colunas mapeadas para uma única tabela

Observe que os tipos das colunas que representam campos nos subtipos precisam ser anuláveis ou precisam ter um padrão especificado. Isso é necessário para que os comandos de inserção sejam bem-sucedidos.

Consultas

O código a seguir fornece um tipo de como você pode usar tipos derivados em suas consultas:

C#

var q = db.Vehicle.Where(p => p is Truck);
//or
var q = db.Vehicle.OfType<Truck>();
//or
var q = db.Vehicle.Select(p => p as Truck).Where(p => p != null);
foreach (Truck p in q)
   Console.WriteLine(p.Axles);

Visual Basic

Dim trucks = From veh In db.Vehicle _ 
             Where TypeOf(veh) Is Truck

For Each truck In trucks
   Console.WriteLine(p.Axles) 
Next

Avançado

Você pode expandir uma hierarquia muito além do exemplo simples já fornecido.

Exemplo 1

Aqui está uma hierarquia muito mais profunda e uma consulta mais complexa:

C#

[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle), IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "D", Type = typeof(DumpTruck))]
public class Truck: Vehicle { ... }

public class Semi: Truck { ... }

public class DumpTruck: Truck { ... }

...
// Get all trucks along with a flag indicating industrial application.

db.Vehicles.OfType<Truck>.Select(t => 
   new {Truck=t, IsIndustrial=t is Semi || t is DumpTruck }
);

Visual Basic

<Table> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), IsDefault:=True)> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="S", Type:=Typeof(Semi))> _
<InheritanceMapping(Code:="D", Type:=Typeof(DumpTruck))> _
Public Class Truck
       InheritsVehicle
Public Class Semi
       Inherits Truck

Public Class DumpTruck
       InheritsTruck 
...
' Get all trucks along with a flag indicating industrial application.
Dim trucks = From veh In db.Vehicle _ 
             Where Typeof(veh) Is Truck And _ 
             IsIndustrial = (Typeof(veh) Is Semi _ 
             Or Typeof(veh) Is DumpTruck)

Exemplo 2

A hierarquia a seguir inclui interfaces:

C#

[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
   IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "H", Type = typeof(Helicopter))]

public class Truck: Vehicle
public class Semi: Truck, IRentableVehicle
public class Helicopter: Vehicle, IRentableVehicle

Visual Basic

<Table> _
<InheritanceMapping(Code:="V", Type:=TypeOf(Vehicle),
   IsDefault:=True) > _
<InheritanceMapping(Code:="C", Type:=TypeOf(Car)) > _
<InheritanceMapping(Code:="T", Type:=TypeOf(Truck)) > _
<InheritanceMapping(Code:="S", Type:=TypeOf(Semi)) > _
<InheritanceMapping(Code:="H", Type:=TypeOf(Helicopter)) > _
Public Class Truck
       Inherits Vehicle
Public Class Semi
       InheritsTruck, IRentableVehicle
Public Class Helicopter
       InheritsVehicle, IRentableVehicle

As possíveis consultas incluem o seguinte:

C#

// Get commercial vehicles ordered by cost to rent.
db.Vehicles.OfType<IRentableVehicle>.OrderBy(cv => cv.RentalRate);

// Get all non-rentable vehicles
db.Vehicles.Where(v => !(v is IRentableVehicle));

Visual Basic

' Get commercial vehicles ordered by cost to rent.
Dim rentableVehicles = From veh In _ 
                       db.Vehicles.OfType(Of IRentableVehicle).OrderBy( _ 
                       Function(cv) cv.RentalRate)

' Get all non-rentable vehicles
Dim unrentableVehicles = From veh In _ 
                         db.Vehicles.OfType(Of Vehicle).Where( _ 
                         Function(uv) Not (TypeOf(uv) Is IRentableVehicle))

Tópicos avançados

Criando bancos de dados

Como as classes de entidade têm atributos que descrevem a estrutura das tabelas e colunas de banco de dados relacionais, é possível usar essas informações para criar novas instâncias do banco de dados. Você pode chamar o método CreateDatabase() no DataContext para que LINQ to SQL construa uma nova instância de banco de dados com uma estrutura definida por seus objetos. Há muitos motivos pelos quais você pode querer fazer isso: você pode estar criando um aplicativo que se instala automaticamente em um sistema cliente ou um aplicativo cliente que precisa de um banco de dados local para salvar seu estado offline. Para esses cenários, CreateDatabase() é ideal, especialmente se um provedor de dados conhecido como SQL Server Express 2005 estiver disponível.

No entanto, os atributos de dados podem não codificar tudo sobre uma estrutura de banco de dados existente. O conteúdo de funções definidas pelo usuário, procedimentos armazenados, gatilhos e restrições marcar não são representados pelos atributos. A função CreateDatabase() criará apenas uma réplica do banco de dados usando as informações que ele sabe, que é a estrutura do banco de dados e os tipos de colunas em cada tabela. No entanto, para uma variedade de bancos de dados, isso é suficiente.

Veja abaixo um exemplo de como você pode criar um novo banco de dados chamado MyDVDs.mdf:

C#

[Table(Name="DVDTable")]
public class DVD
{
   [Column(Id = true)]
   public string Title;
   [Column]
   public string Rating;
}

public class MyDVDs : DataContext
{
   public Table<DVD> DVDs;

   public MyDVDs(string connection) : base(connection) {}
}

Visual Basic

<Table(Name:="DVDTable")> _
Public Class DVD

   <Column(Id:=True)> _
   public Title As String
   <Column> _
   Public Rating As String
End Class

Public Class MyDVDs  
         Inherits DataContext

   Public DVDs As Table(Of DVD)

   Public Sub New(connection As String) 
End Class

O modelo de objeto pode ser usado para criar um banco de dados usando SQL Server Express 2005 da seguinte maneira:

C#

MyDVDs db = new MyDVDs("c:\\mydvds.mdf");
db.CreateDatabase();

Visual Basic

Dim db As MyDVDs = new MyDVDs("c:\mydvds.mdf")
db.CreateDatabase()

LINQ to SQL também fornece uma API para remover um banco de dados existente antes de criar um novo. O código de criação de banco de dados acima pode ser modificado para primeiro marcar para uma versão existente do banco de dados usando DatabaseExists() e, em seguida, soltá-lo usando DeleteDatabase().

C#

MyDVDs db = new MyDVDs("c:\\mydvds.mdf");

if (db.DatabaseExists()) {
   Console.WriteLine("Deleting old database...");
   db.DeleteDatabase();
}

db.CreateDatabase();

Visual Basic

Dim db As MyDVDs = New MyDVDs("c:\mydvds.mdf")

If (db.DatabaseExists()) Then
   Console.WriteLine("Deleting old database...")
   db.DeleteDatabase()
End If

db.CreateDatabase()

Após a chamada para CreateDatabase(), o novo banco de dados é capaz de aceitar consultas e comandos como SubmitChanges() para adicionar objetos ao arquivo MDF.

Também é possível usar CreateDatabase() com um SKU diferente de SQL Server Express, usando um arquivo MDF ou apenas um nome de catálogo. Tudo depende do que você usa para sua cadeia de conexão. As informações na cadeia de conexão são usadas para definir o banco de dados que existirá, não necessariamente um que já existe. LINQ to SQL descobrirá os bits de informações relevantes e as usará para determinar qual banco de dados criar e em qual servidor criá-lo. É claro que você precisará de direitos de administrador de banco de dados ou equivalentes no servidor para fazer isso.

Interoperando com ADO.NET

O LINQ to SQL faz parte da família de tecnologias ADO.NET. Ele se baseia nos serviços fornecidos pelo modelo de provedor ADO.NET, portanto, é possível misturar LINQ to SQL código com aplicativos ADO.NET existentes.

Ao criar um LINQ to SQL DataContext, você pode fornecê-lo com uma conexão ADO.NET existente. Todas as operações no DataContext, incluindo consultas, usarão a conexão fornecida. Se a conexão já tiver sido aberta LINQ to SQL honrará sua autoridade sobre a conexão e a deixará como está quando terminar com ela. Normalmente, LINQ to SQL fecha sua conexão assim que uma operação é concluída, a menos que uma transação esteja no escopo.

C#

SqlConnection con = new SqlConnection( ... );
con.Open(); 
...

// DataContext takes a connection
Northwind db = new Northwind(con);
...

var q =
   from c in db.Customers
   where c.City == "London"
   select c;

Visual Basic

Dim con As SqlConnection = New SqlConnection( ... )
con.Open()
...

' DataContext takes a connection
Dim db As Northwind = new Northwind(con)
...

Dim q = From c In db.Customers _
        Where c.City = "London" _
        Select c

Você sempre pode acessar a conexão usada por seu DataContext por meio da propriedade Connection e fechá-la por conta própria.

C#

db.Connection.Close(); 

Visual Basic

db.Connection.Close()

Você também pode fornecer o DataContext com sua própria transação de banco de dados, caso seu aplicativo já tenha iniciado um e você desejá-lo.

C#

IDbTransaction = con.BeginTransaction();
...

db.Transaction = myTransaction;
db.SubmitChanges();
db.Transaction = null;

Visual Basic

Dim db As IDbTransaction = con.BeginTransaction()
...

db.Transaction = myTransaction
db.SubmitChanges()
db.Transaction = Nothing

Sempre que uma Transação for definida, o DataContext a usará sempre que emitir uma consulta ou executar um comando. Não se esqueça de atribuir a propriedade de volta a nulo quando terminar.

No entanto, o método preferencial de fazer transações com o .NET Framework é usar o objeto TransactionScope. Ele permite que você faça transações distribuídas que funcionam entre bancos de dados e outros gerenciadores de recursos residentes em memória. A ideia é que os escopos de transação comecem baratos, promovendo-se apenas para a transação distribuída completa quando realmente se referem a vários bancos de dados ou várias conexões dentro do escopo da transação.

C#

using(TransactionScope ts = new TransactionScope()) {
   db.SubmitChanges();
   ts.Complete();
}

Visual Basic

Using ts As TransactionScope= New TransactionScope()
   db.SubmitChanges()
   ts.Complete()
End Using

Executando instruções SQL diretamente

Conexões e transações não são a única maneira de interoperar com ADO.NET. Você pode descobrir que, em alguns casos, o recurso de consulta ou envio de alterações do DataContext é insuficiente para a tarefa especializada que talvez você queira executar. Nessas circunstâncias, é possível usar o DataContext para emitir comandos SQL brutos diretamente para o banco de dados.

O método ExecuteQuery() permite executar uma consulta SQL bruta e converte o resultado da consulta diretamente em objetos . Por exemplo, supondo que os dados da classe Customer sejam distribuídos em duas tabelas customer1 e customer2, a consulta a seguir retorna uma sequência de objetos Customer .

C#

IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
   @"select c1.custid as CustomerID, c2.custName as ContactName
      from customer1 as c1, customer2 as c2
      where c1.custid = c2.custid"
);

Visual Basic

Dim results As IEnumerable(Of Customer) = _
          db.ExecuteQuery(Of Customer)( _
          "select c1.custid as CustomerID, " & _
          "c2.custName as ContactName " & _
          "from customer1 as c1, customer2 as c2 "& _
          "where c1.custid = c2.custid" )

Desde que os nomes de coluna nos resultados tabulares correspondam às propriedades da coluna da classe de entidade LINQ to SQL materializará seus objetos de qualquer consulta SQL.

O método ExecuteQuery() também permite parâmetros. No código a seguir, uma consulta parametrizada é executada:

C#

IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
   "select contactname from customers where city = {0}",
   "London"
);

Visual Basic

Dim results As IEnumerable(Of Customer) = _
          db.ExecuteQuery(Of Customer)( _
   "select contactname from customers where city = {0}", _
   "London" )

Os parâmetros são expressos no texto da consulta usando a mesma notação curly usada por Console.WriteLine() e String.Format(). Na verdade, String.Format() é realmente chamado na cadeia de caracteres de consulta que você fornece, substituindo os parâmetros com chaves por nomes de parâmetro gerados como p0, @p1 ..., p(n).

Alterar resolução de conflitos

Descrição

Um conflito de alteração ocorre quando o cliente tenta enviar alterações a um objeto e um ou mais valores usados na atualização marcar foram atualizados no banco de dados desde a última leitura do cliente.

Nota Somente membros mapeados como UpdateCheck.Always ou UpdateCheck.WhenChanged participam de verificações de simultaneidade otimistas. Nenhum marcar é executado para membros marcados como UpdateCheck.Never.

A resolução desse conflito inclui descobrir quais membros do objeto estão em conflito e, em seguida, decidir o que fazer sobre ele. Observe que a simultaneidade otimista pode não ser a melhor estratégia em sua situação específica. Às vezes, é perfeitamente razoável "deixar a última atualização ganhar".

Detectando, relatando e resolvendo conflitos no LINQ to SQL

A resolução de conflitos é o processo de atualizar um item conflitante consultando o banco de dados novamente e reconciliando quaisquer diferenças. Quando um objeto é atualizado, o rastreador de alterações tem os valores originais antigos e os novos valores de banco de dados. LINQ to SQL determina se o objeto está em conflito ou não. Se for, LINQ to SQL determina quais membros estão envolvidos. Se o novo valor de banco de dados para um membro for diferente do original antigo (que foi usado para a atualização marcar que falhou), esse será um conflito. Todos os conflitos de membro são adicionados a uma lista de conflitos.

Por exemplo, no cenário a seguir, User1 começa a preparar uma atualização consultando o banco de dados para uma linha. Antes que o User1 possa enviar as alterações, o User2 alterou o banco de dados. O envio de User1 falha porque os valores esperados para Col B e Col C foram alterados.

Conflito de atualização de banco de dados

Usuário Col A Col B Col C
Estado original Alfreds Maria Sales
Usuário 1 Alfred   Marketing
Usuário 2   Mary Serviço

Em LINQ to SQL, objetos que não são atualizados devido a conflitos de simultaneidade otimistas fazem com que uma exceção (ChangeConflictException) seja gerada. Você pode especificar se a exceção deve ser gerada na primeira falha ou se todas as atualizações devem ser tentadas com quaisquer falhas sendo acumuladas e relatadas na exceção.

// [C#]
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
db.SubmitChanges(ConflictMode.ContinueOnConflict);
' [Visual Basic]
db.SubmitChanges(ConflictMode.FailOnFirstConflict)
db.SubmitChanges(ConflictMode.ContinueOnConflict)

Quando gerada, a exceção fornece acesso a uma coleção ObjectChangeConflict . Os detalhes estão disponíveis para cada conflito (mapeado para uma única tentativa de atualização com falha), incluindo o acesso à lista MemberConflicts . Mapas de cada conflito de membro a um único membro na atualização que falhou a verificação de simultaneidade.

Tratamento de conflitos

No cenário anterior, User1 tem as opções RefreshMode descritas abaixo para reconciliar as diferenças antes de tentar reenviar. Em todos os casos, o registro no cliente é primeiro "atualizado" efetuando pull dos dados atualizados do banco de dados. Essa ação garante que a próxima tentativa de atualização não falhará nas mesmas verificações de simultaneidade.

Aqui, User1 opta por mesclar valores de banco de dados com os valores atuais do cliente para que os valores de banco de dados sejam substituídos somente quando o conjunto de alterações atual também tiver modificado esse valor. (Consulte o Exemplo 1 mais adiante nesta seção.)

No cenário acima, após a resolução de conflitos, o resultado no banco de dados é o seguinte:

KeepChanges

  Col A Col B Col C
KeepChanges Alfred (Usuário 1) Mary (Usuário 2) Marketing (Usuário 1)
  • Col A: a alteração de User1 (Alfred) é exibida.
  • Col B: a alteração de User2 (Maria) é exibida. Esse valor foi mesclado porque User1 não o alterou.
  • Col C: a alteração de User1 (Marketing) é exibida. A alteração do User2 (Serviço) não é mesclada porque o User1 também alterou esse item.

Abaixo, User1 opta por substituir todos os valores de banco de dados com os valores atuais. (Confira o exemplo 2 mais adiante nesta seção.)

Após a atualização, as alterações de User1 são enviadas. O resultado no banco de dados é o seguinte:

KeepCurrentValues

  Col A Col B Col C
KeepCurrentValues Alfred (Usuário 1) Maria (Original) Marketing (Usuário 1)
  • Col A: a alteração de User1 (Alfred) é exibida.
  • Col B: A Maria original permanece; A alteração de User2 é descartada.
  • Col C: a alteração de User1 (Marketing) é exibida. A alteração de User2 (Serviço) é descartada.

No próximo cenário, User1 opta por permitir que os valores de banco de dados substituam os valores atuais no cliente. (Confira o exemplo 3 mais adiante nesta seção.)

No cenário acima, após a resolução de conflitos, o resultado no banco de dados é o seguinte:

OverwriteCurrentValues

  Col A Col B Col C
OverwriteCurrentValues Alfreds (Original) Mary (Usuário 2) Serviço (Usuário 2)
  • Col A: o valor original (Alfreds) permanece; O valor de User1 (Alfred) é descartado.
  • Col B: a alteração de User2 (Mary) é exibida.
  • Col C: a alteração de User2 (Serviço) é exibida. A alteração do User1 (Marketing) é descartada.

Depois que os conflitos forem resolvidos, você poderá tentar reenviar. Como essa segunda atualização também pode falhar, considere usar um loop para tentativas de atualização.

Exemplos

Os trechos de código a seguir mostram vários membros informativos e técnicas à sua disposição para descobrir e resolver conflitos de membros.

Exemplo 1

Neste exemplo, os conflitos são resolvidos "automaticamente". Ou seja, os valores de banco de dados são mesclados com os valores atuais do cliente, a menos que o cliente também tenha alterado esse valor (KeepChanges). Nenhuma inspeção ou tratamento personalizado de conflitos de membros individuais ocorre.

C#

try {
   context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
   //automerge database values into current for members
   //that client has not modified
   context.ChangeConflicts.Resolve(RefreshMode.KeepChanges);
}
//submit succeeds on second try
context.SubmitChanges(ConflictMode.FailOnFirstConflict);

Visual Basic

Try 
   context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
   ' automerge database values into current for members
   ' that client has not modified   context.ChangeConflicts.Resolve(RefreshMode.KeepChanges)
End Try
' submit succeeds on second try
context.SubmitChanges(ConflictMode.FailOnFirstConflict)

Exemplo 2

Neste exemplo, os conflitos são resolvidos novamente sem tratamento personalizado. Mas, desta vez, os valores de banco de dados não são mesclados nos valores atuais do cliente.

C#

try {
   context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
   foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
      //No database values are automerged into current
      cc.Resolve(RefreshMode.KeepCurrentValues);
   }
}

Visual Basic

Try 
   context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
   For Each cc As ObjectChangeConflict In context.ChangeConflicts
      ‘No database values are automerged into current
      cc.Resolve(RefreshMode.KeepCurrentValues)
   Next
End Try

Exemplo 3

Aqui novamente, nenhuma manipulação personalizada ocorre. Mas, nesse caso, todos os valores de cliente são atualizados com os valores atuais do banco de dados.

C#

try {
   context.SubmitChanges(ConflictMode.ContinueOnConflict); 
}
catch (ChangeConflictException e) {
   foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
      //No database values are automerged into current
      cc.Resolve(RefreshMode.OverwriteCurrentValues);
   }
}

Visual Basic

Try 
   context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
   For Each cc As ObjectChangeConflict In context.ChangeConflicts
      ' No database values are automerged into current
      cc.Resolve(RefreshMode. OverwriteCurrentValues)
   Next
End Try

Exemplo 4

Este exemplo mostra uma maneira de acessar informações sobre uma entidade em conflito.

C#

try {
   user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
   Console.WriteLine("Optimistic concurrency error");
   Console.ReadLine();
   foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
      ITable table = cc.Table;
      Customers entityInConflict = (Customers)cc.Object;
      Console.WriteLine("Table name: {0}", table.Name);
      Console.Write("Customer ID: ");
      Console.WriteLine(entityInConflict.CustomerID);
   }
}

Visual Basic

Try 
   context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
   Console.WriteLine("Optimistic concurrency error")
   Console.ReadLine()
   For Each cc As ObjectChangeConflict In context.ChangeConflicts
      Dim table As ITable = cc.Table
      Dim entityInConflict As Customers = CType(cc.Object, Customers)
      Console.WriteLine("Table name: {0}", table.Name)
      Console.Write("Customer ID: ")
      Console.WriteLine(entityInConflict.CustomerID)
   Next
End Try

Exemplo 5

Este exemplo adiciona um loop por meio dos membros individuais. Aqui você pode fornecer tratamento personalizado de qualquer membro.

Nota Adicionar usando System.Reflection; para fornecer MemberInfo.

C#

try {
   user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
   Console.WriteLine("Optimistic concurrency error");
   Console.ReadLine();
   foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
      ITable table = cc.Table;
      Customers entityInConflict = (Customers)cc.Object;
      Console.WriteLine("Table name: {0}", table.Name);
      Console.Write("Customer ID: ");
      Console.WriteLine(entityInConflict.CustomerID);
      foreach (MemberChangeConflict mc in         cc.MemberConflicts) {
         object currVal = mc.CurrentValue;
         object origVal = mc.OriginalValue;
         object databaseVal = mc.DatabaseValue;
         MemberInfo mi = mc. Member;
         Console.WriteLine("Member: {0}", mi.Name);
         Console.WriteLine("current value: {0}", currVal);
         Console.WriteLine("original value: {0}", origVal);
         Console.WriteLine("database value: {0}", databaseVal);
         Console.ReadLine();
      }
   }
}

Visual Basic

Try 
   user1.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
   Console.WriteLine("Optimistic concurrency error")
   Console.ReadLine()
   For Each cc As ObjectChangeConflict In context.ChangeConflicts
      Dim table As ITable = cc.Table
      Dim entityInConflict As Customers = CType(cc.Object, Customers)
      Console.WriteLine("Table name: {0}", table.Name)
      Console.Write("Customer ID: ")
      Console.WriteLine(entityInConflict.CustomerID)
   For Each mc As MemberChangeConflict In   cc.MemberConflicts
         Dim currVal As Object = mc.CurrentValue
         Dim origVal As Object = mc.OriginalValue
         Dim databaseVal As Object = mc.DatabaseValue
         Dim mi As MemberInfo = mc.Member
         Console.WriteLine("Member: {0}", mi.Name)
         Console.WriteLine("current value: {0}", currVal)
         Console.WriteLine("original value: {0}", origVal)
         Console.WriteLine("database value: {0}", databaseVal)
         Console.ReadLine()
      Next

   Next
End Try

Invocação de Procedimentos Armazenados

O LINQ to SQL dá suporte a procedimentos armazenados e a funções definidas pelo usuário. LINQ to SQL mapeia essas abstrações definidas pelo banco de dados para objetos cliente gerados por código, para que você possa acessá-las de maneira fortemente tipada do código do cliente. Você pode descobrir facilmente esses métodos usando o IntelliSense e as assinaturas de método se assemelham o mais próximo possível das assinaturas dos procedimentos e funções definidos no banco de dados. Um conjunto de resultados retornado por uma chamada para um procedimento mapeado é uma coleção fortemente tipada. LINQ to SQL pode gerar automaticamente os métodos mapeados, mas também dá suporte ao mapeamento manual em situações em que você opta por não usar a geração de código.

LINQ to SQL mapeia procedimentos armazenados e funções para métodos por meio do uso de atributos. Os atributos StoredProcedure, Parameter e Function dão suporte a uma propriedade Name e o atributo Parameter também dá suporte a uma propriedade DBType . Veja dois exemplos:

C#

   [StoredProcedure()]
   public IEnumerable<CustOrderHistResult> CustOrderHist(
      [Parameter(Name="CustomerID", DBType="NChar(5)")] string customerID) {

      IExecuteResult result = this.ExecuteMethodCall(this, 
         ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);

      return ((IEnumerable<CustOrderHistResult>)(result.ReturnValue));
   }

[Function(Name="[dbo].[ConvertTemp]")]
public string ConvertTemp(string string) { ... }

Visual Basic

<StoredProcedure()> _
   Public Function CustOrderHist( _
         <Parameter(Name:="CustomerID", DBType:="NChar(5)")> _
         customerID As String) As IEnumerable(Of CustOrderHistResult)

         Dim result As IExecuteResult = ExecuteMethodCall(Me, _
                 CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)

         Return CType(result.ReturnValue, IEnumerable(Of CustOrderHistResult))
   End Function

<Function(Name:="[dbo].[ConvertTemp]")> _
Public Function ConvertTemp(str As String) As String

Os exemplos a seguir mostram mapeamentos para vários tipos de procedimentos armazenados.

Exemplo 1

O procedimento armazenado a seguir usa um único parâmetro de entrada e retorna um inteiro:

CREATE PROCEDURE GetCustomerOrderCount(@CustomerID nchar(5))
AS
Declare @count int
SELECT @count = COUNT(*) FROM ORDERS WHERE CustomerID = @CustomerID
RETURN @count

O método mapeado seria o seguinte:

C#

[StoredProcedure(Name = "GetCustomerOrderCount")]
public int GetCustomerOrderCount(
      [Parameter(Name = "CustomerID")] string customerID) {
         IExecuteResult result = this.ExecuteMethodCall(this, 
            ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID); 
          return (int) result.ReturnValue;
}

Visual Basic

<StoredProcedure (Name:="GetCustomerOrderCount")> _
public Function GetCustomerOrderCount( _
      <Parameter(Name:= "CustomerID")> customerID As String) As Integer
         Dim result As IExecuteResult = ExecuteMethodCall(Me, _
               CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
          return CInt(result.ReturnValue)
End Function

Exemplo 2

Quando um procedimento armazenado pode retornar várias formas de resultado, o tipo de retorno não pode ser fortemente tipado a uma única forma de projeção. No exemplo a seguir, a forma de resultado depende da entrada:

CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
   select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
   select OrderID, ShipName from orders

O método mapeado é o seguinte:

C#

      [StoredProcedure(Name = "VariableResultShapes")]
      [ResultType(typeof(Customer))]
      [ResultType(typeof(Order))]
      public IMultipleResults VariableResultShapes(System.Nullable<int> shape) {
         IExecuteResult result = this.ExecuteMethodCallWithMultipleResults(this,
            ((MethodInfo)(MethodInfo.GetCurrentMethod())), shape);
         return (IMultipleResults) result.ReturnValue;
      }

Visual Basic

<StoredProcedure(Name:= "VariableResultShapes")> _
      <ResultType(typeof(Customer))> _
      <ResultType(typeof(Order))> _
   public VariableResultShapes(shape As Integer?) As IMultipleResults
      Dim result As IExecuteResult =
                ExecuteMethodCallWithMultipleResults(Me, _
               CType(MethodInfo.GetCurrentMethod(), MethodInfo), shape)
         return CType(result.ReturnValue, IMultipleResults)
      End Function

Você pode usar esse procedimento armazenado da seguinte maneira:

C#

      IMultipleResults result = db.VariableResultShapes(1);
      foreach (Customer c in result.GetResult<Customer>()) {
         Console.WriteLine(c.CompanyName);
      }

      result = db.VariableResultShapes(2);
      foreach (Order o in result.GetResult<Order>()) {
         Console.WriteLine(o.OrderID);
      }           

Visual Basic

Dim result As IMultipleResults = db.VariableResultShapes(1)
      For Each c As Customer In result.GetResult(Of Customer)()
         Console.WriteLine(c.CompanyName)
      Next

      result = db.VariableResultShapes(2);
      For Each o As Order In result.GetResult(Of Order)()
         Console.WriteLine(o.OrderID)
      Next 
         
      }           

Aqui, você precisa usar o padrão GetResult para obter um enumerador do tipo correto, com base em seu conhecimento sobre o procedimento armazenado. LINQ to SQL pode gerar todos os tipos de projeção possíveis, mas não tem como saber em que ordem eles serão retornados. A única maneira de saber quais tipos de projeção gerados correspondem a um método mapeado é usando comentários de código gerados nos métodos.

Exemplo 3

Aqui está o T-SQL de um procedimento armazenado que retorna várias formas de resultado sequencialmente:

CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers

LINQ to SQL mapearia esse procedimento, assim como no Exemplo 2 acima. Nesse caso, no entanto, há dois conjuntos de resultados sequenciais .

C#

[StoredProcedure(Name="MultipleResultTypesSequentially")]      
[ResultType(typeof(Product))]
[ResultType(typeof(Customer))]
public IMultipleResults MultipleResultTypesSequentially() {
   return ((IMultipleResults)(
      this.ExecuteMethodCallWithMultipleResults (this, 
         ((MethodInfo)(MethodInfo.GetCurrentMethod()))).ReturnValue
      )
   );
}

Visual Basic

<StoredProcedure(Name:="MultipleResultTypesSequentially")> _
<ResultType(typeof(Customer))> _
<ResultType(typeof(Order))> _
public Function MultipleResultTypesSequentially() As IMultipleResults
   Return CType( ExecuteMethodCallWithMultipleResults (Me, _
         CType(MethodInfo.GetCurrentMethod(), MethodInfo)), _
         IMultipleResults).ReturnValue
      
End Function

Você pode usar esse procedimento armazenado da seguinte maneira:

C#

      IMultipleResults sprocResults = db.MultipleResultTypesSequentially();

      //first read products
      foreach (Product p in sprocResults.GetResult<Product>()) {
         Console.WriteLine(p.ProductID);
      }

      //next read customers
      foreach (Customer c in sprocResults.GetResult<Customer>()){
         Console.WriteLine(c.CustomerID);
      }

Visual Basic

Dim sprocResults As IMultipleResults = db.MultipleResultTypesSequentially()

   ' first read products
   For Each P As Product In sprocResults.GetResult(Of Product)()
      Console.WriteLine(p.ProductID)
   Next

   ' next read customers
   For Each c As Customer c In sprocResults.GetResult(Of Customer)()
      Console.WriteLine(c.CustomerID) 
   Next

Exemplo 4

LINQ to SQL mapeia out parâmetros para parâmetros de referência (palavra-chave ref) e para tipos de valor declara o parâmetro como anulável (por exemplo, int?). O procedimento no exemplo a seguir usa um único parâmetro de entrada e retorna um out parâmetro .

CREATE PROCEDURE GetCustomerCompanyName(
   @customerID nchar(5),
   @companyName nvarchar(40) output
   )
AS
SELECT @companyName = CompanyName FROM Customers
WHERE CustomerID=@CustomerID

O método mapeado é o seguinte:

C#

      [StoredProcedure(Name = "GetCustomerCompanyName")]
      public int GetCustomerCompanyName(
         string customerID, ref string companyName) {

         IExecuteResult result =
            this.ExecuteMethodCall(this,
               ((MethodInfo)(MethodInfo.GetCurrentMethod())),
               customerID, companyName);

         companyName = (string)result.GetParameterValue(1);
         return (int)result.ReturnValue;
      }

Visual Basic

   <StoredProcedure(Name:="GetCustomerCompanyName")> _
      Public Function GetCustomerCompanyName( _
               customerID As String, ByRef companyName As String) As Integer

      Dim result As IExecuteResult = ExecuteMethodCall(Me, _
               CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID, _
               companyName)

         companyName = CStr(result.GetParameterValue(1))
         return CInt(result.ReturnValue)
      End Function

Nesse caso, o método não tem um valor retornado explícito, mas o valor retornado padrão é mapeado mesmo assim. Para o parâmetro de saída, um parâmetro de saída correspondente é usado conforme o esperado.

Você chamaria o procedimento armazenado acima da seguinte maneira:

C#

string CompanyName = "";
string customerID = "ALFKI";
db.GetCustomerCompanyName(customerID, ref CompanyName);
Console.WriteLine(CompanyName);

Visual Basic

Dim CompanyName As String = ""
Dim customerID As String = "ALFKI"
db.GetCustomerCompanyName(customerID, CompanyName)
Console.WriteLine(CompanyName)

Funções definidas pelo usuário

LINQ to SQL dá suporte a funções com valor escalar e com valor de tabela e dá suporte ao equivalente em linha de ambos.

LINQ to SQL lida com chamadas escalares embutidas da mesma forma que as funções definidas pelo sistema são chamadas. Considere a consulta a seguir:

C#

var q =
   from p in db.Products
   select
      new {
         pid = p.ProductID,
         unitp = Math.Floor(p.UnitPrice.Value)
      };

Visual Basic

Dim productInfos = From prod In db.Products _
                   Select p.ProductID, price = Math.Floor(p.UnitPrice.Value)

Aqui, a chamada de método Math.Floor é convertida em uma chamada para a função do sistema 'FLOOR'. Da mesma forma, uma chamada para uma função mapeada para uma UDF é convertida em uma chamada para a UDF no SQL.

Exemplo 1

Aqui está uma função escalar definida pelo usuário (UDF) ReverseCustName(). Em SQL Server, a função pode ser definida da seguinte maneira:

CREATE FUNCTION ReverseCustName(@string varchar(100))
RETURNS varchar(100)
AS
BEGIN
   DECLARE @custName varchar(100)
   -- Impl. left as exercise for the reader
   RETURN @custName
END

Você pode mapear um método de cliente definido em uma classe de esquema para essa UDF usando o código abaixo. Observe que o corpo do método constrói uma expressão que captura a intenção da chamada de método e passa essa expressão para o DataContext para tradução e execução. (Essa execução direta ocorrerá somente se a função for chamada.)

C#

[Function(Name = "[dbo].[ReverseCustName]")]
public string ReverseCustName(string string1) {
   IExecuteResult result = this.ExecuteMethodCall(this,
      (MethodInfo)(MethodInfo.GetCurrentMethod())), string1);
   return (string) result.ReturnValue;
}

Visual Basic

Function(Name:= "[dbo].[ReverseCustName]")> _
Public Function ReverseCustName(string1 As String) As String

    Dim result As IExecuteResult = ExecuteMethodCall(Me, _
             CType(MethodInfo.GetCurrentMethod(), MethodInfo), string1)
   return CStr(result.ReturnValue)

Exemplo 2

Na consulta a seguir, você pode ver uma chamada embutida para o método UDF gerado ReverseCustName. Nesse caso, a função não é executada imediatamente. O SQL criado para essa consulta é convertido em uma chamada para a UDF definida no banco de dados (consulte o código SQL após a consulta).

C#

var q =
   from c in db.Customers
   select
      new {
         c.ContactName,
         Title = db.ReverseCustName(c.ContactTitle)
      };

Visual Basic

Dim customerInfos = From cust In db.Customers _
                    Select c.ContactName, _
                    Title = db.ReverseCustName(c.ContactTitle)



SELECT [t0].[ContactName],
   dbo.ReverseCustName([t0].[ContactTitle]) AS [Title]
FROM [Customers] AS [t0]

Quando você chama a mesma função fora de uma consulta, LINQ to SQL cria uma consulta simples da expressão de chamada de método com a seguinte sintaxe SQL (em que o parâmetro @p0 está associado à constante passada):

Em LINQ to SQL:

C#

string str = db.ReverseCustName("LINQ to SQL");

Visual Basic

Dim str As String = db.ReverseCustName("LINQ to SQL")

Converte em:

SELECT dbo.ReverseCustName(@p0)

Exemplo 3

Uma TVF (função com valor de tabela) retorna um único conjunto de resultados (ao contrário dos procedimentos armazenados, que podem retornar várias formas de resultado). Como o tipo de retorno TVF é table, você pode usar um TVF em qualquer lugar no SQL que você pode usar uma tabela e pode tratar o TVF da mesma maneira que faria com uma tabela.

Considere a seguinte definição SQL Server de uma função com valor de tabela:

CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
   SELECT ProductID, UnitPrice
   FROM Products
   WHERE UnitPrice > @cost

Essa função declara explicitamente que retorna um TABLE, portanto, a estrutura do conjunto de resultados retornado é definida implicitamente. O LINQ to SQL mapeia a função da seguinte maneira:

C#

       [Function(Name = "[dbo].[ProductsCostingMoreThan]")]
      public IQueryable<Product> ProductsCostingMoreThan(
            System.Nullable<decimal> cost) {

         return this.CreateMethodCallQuery<Product>(this,
            (MethodInfo)MethodInfo.GetCurrentMethod(),
            cost);
      }

Visual Basic

   <Function(Name:="[dbo].[ProductsCostingMoreThan]")> _
      Public Function ProductsCostingMoreThan(
            cost As System.Nullable(Of Decimal)) As IQueryable(Of Product)

    Return CreateMethodCallQuery(Of Product)(Me, _
             CType(MethodInfo.GetCurrentMethod(), MethodInfo), cost)

O código SQL a seguir mostra que você pode ingressar na tabela retornada pela função e tratá-la como faria com qualquer outra tabela:

SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID

Em LINQ to SQL, a consulta seria renderizada da seguinte maneira (usando a nova sintaxe 'join'):

C#

var q =
   from p in db.ProductsCostingMoreThan(80.50m)
   join s in db.Products on p.ProductID equals s.ProductID
   select new {p.ProductID, s.UnitPrice};

Visual Basic

Dim productInfos = From costlyProd In db.ProductsCostingMoreThan(80.50m) _
                   Join prod In db.Products _
                   On costlyProd.ProductID Equals prod.ProductID _
                   Select costlyProd.ProductID, prod.UnitPrice

limitações de LINQ to SQL em procedimentos armazenados

LINQ to SQL dá suporte à geração de código para procedimentos armazenados que retornam conjuntos de resultados determinados estaticamente. Portanto, o gerador de código LINQ to SQL não dá suporte ao seguinte:

  • Procedimentos armazenados que usam SQL dinâmico para retornar conjuntos de resultados. Quando um procedimento armazenado contém lógica condicional para criar uma instrução SQL dinâmica, LINQ to SQL não pode adquirir metadados para o conjunto de resultados porque a consulta usada para gerar o conjunto de resultados é desconhecida até o tempo de execução.
  • Procedimentos armazenados que produzem resultados com base na tabela temporária.

A ferramenta geradora de classe de entidade

Se você tiver um banco de dados existente, será desnecessário criar um modelo de objeto completo manualmente apenas para representá-lo. A distribuição LINQ to SQL vem com uma ferramenta chamada SQLMetal. É um utilitário de linha de comando que automatiza a tarefa de criar classes de entidade inferindo as classes apropriadas dos metadados do banco de dados.

Você pode usar o SQLMetal para extrair metadados SQL de um banco de dados e gerar um arquivo de origem contendo declarações de classe de entidade. Como alternativa, você pode dividir o processo em duas etapas, primeiro gerando um arquivo XML que representa os metadados sql e, em seguida, convertendo esse arquivo XML em um arquivo de origem que contém declarações de classe. Esse processo de divisão permite que você mantenha os metadados como um arquivo para que você possa editá-lo. O processo de extração que produz o arquivo faz algumas inferências ao longo do caminho sobre nomes de classe e propriedade apropriados, considerando os nomes de tabela e coluna do banco de dados. Talvez seja necessário editar o arquivo XML para que o gerador produza resultados mais agradáveis ou ocultar aspectos do banco de dados que você não deseja que estejam presentes em seus objetos.

O cenário mais simples para usar o SQLMetal é gerar classes diretamente de um banco de dados existente. Veja como invocar a ferramenta:

C#

SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.cs

Visual Basic

SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.vb /language:vb

A execução da ferramenta cria um arquivo Northwind.cs ou .vb que contém o modelo de objeto gerado lendo os metadados do banco de dados. Esse uso funcionará bem se os nomes das tabelas no banco de dados forem semelhantes aos nomes dos objetos que você deseja gerar. Caso contrário, você desejará adotar a abordagem de duas etapas.

Para instruir o SQLMetal a gerar um arquivo DBML, use a ferramenta da seguinte maneira:

SqlMetal /server:.\SQLExpress /database:Northwind /pluralize
   /xml:Northwind.dbml

Depois que o arquivo dbml é gerado, você pode ir em frente e anotá-lo com atributo de classe e propriedade para descrever como tabelas e colunas são mapeadas para classes e propriedades. Depois de terminar de anotar o arquivo dbml, você poderá gerar o modelo de objeto executando o seguinte comando:

C#

SqlMetal /namespace:nwind /code:Northwind.cs Northwind.dbml

Visual Basic

SqlMetal /namespace:nwind /code:Northwind.vb Northwind.dbml /language:vb

A assinatura de uso do SQLMetal é a seguinte:

SqlMetal [options] [filename]

Veja a seguir uma tabela mostrando as opções de linha de comando disponíveis para SQLMetal.

Opções de linha de comando para SQLMetal

Opção Descrição
/server:<name> Indica o servidor ao qual se conectar para acessar o banco de dados.
/database:<name> Indica o nome do banco de dados do qual ler metadados.
/user:<name> ID de usuário de logon para o servidor.
/password:<name> Senha de logon para o servidor.
/views Extrair exibições de banco de dados.
/functions Extrair funções de banco de dados.
/sprocs Extrair procedimentos armazenados.
/code[:<filename>] Indica que a saída da ferramenta é um arquivo de origem de declarações de classe de entidade.
/language:<language> Use Visual Basic ou C# (padrão).
/xml[:<filename>] Indica que a saída das ferramentas é um arquivo DBML que descreve os metadados do banco de dados e a primeira aproximação de adivinhação de nomes de classe e propriedade.
/map[:<filename>] Indica que um arquivo de mapeamento externo deve ser usado em vez de atributos.
/pluralize Indica que a ferramenta deve executar a heurística de pluralização/des pluralização de idioma inglês para os nomes das tabelas, a fim de produzir nomes de classe e propriedade apropriados.
/namespace:<name> Indica o namespace no qual as classes de entidade serão geradas.
/timeout:<seconds> Valor de tempo limite em segundos a ser usado para comandos de banco de dados.

Nota Para extrair os metadados de um arquivo MDF, você deve especificar o nome do arquivo MDF após todas as outras opções. Se nenhum localhost/server for especificado, será assumido.

Referência DBML da ferramenta geradora

O arquivo DBML (Linguagem de Mapeamento de Banco de Dados) é, acima de tudo, uma descrição dos metadados sql de um determinado banco de dados. Ele é extraído pelo SQLMetal examinando os metadados do banco de dados. O mesmo arquivo também é usado pelo SQLMetal para gerar um modelo de objeto padrão para representar o banco de dados.

Aqui está um exemplo prototípico da sintaxe DBML:

<?xml version="1.0" encoding="utf-16"?>
<Database Name="Northwind" EntityNamespace="Mappings.FunctionMapping"
   ContextNamespace="Mappings.FunctionMapping"
   Provider="System.Data.Linq.SqlClient.Sql2005Provider"
   xmlns="https://schemas.microsoft.com/dsltools/LINQ to SQLML">
   <Table Name="Categories">
      <Type Name="Category">
         <Column Name="CategoryID" Type="System.Int32"
            DbType="Int NOT NULL IDENTITY" IsReadOnly="False" 
            IsPrimaryKey="True" IsDbGenerated="True" CanBeNull="False" />
         <Column Name="CategoryName" Type="System.String"
            DbType="NVarChar(15) NOT NULL" CanBeNull="False" />
         <Column Name="Description" Type="System.String"
            DbType="NText" CanBeNull="True" UpdateCheck="Never" />
         <Column Name="Picture" Type="System.Byte[]"
            DbType="Image" CanBeNull="True" UpdateCheck="Never" />
         <Association Name="FK_Products_Categories" Member="Products"
            ThisKey="CategoryID" OtherKey="CategoryID"
            OtherTable="Products" DeleteRule="NO ACTION" />
      </Type>
   </Table>

   <Function Name="GetCustomerOrders">
      <Parameter Name="customerID" Type="System.String" DbType="NChar(5)" />
      <ElementType Name="GetCustomerOrdersResult">
         <Column Name="OrderID" Type="System.Int32"
            DbType="Int" CanBeNull="True" />
         <Column Name="ShipName" Type="System.String"
            DbType="NVarChar(40)" CanBeNull="True" />
         <Column Name="OrderDate" Type="System.DateTime"
            DbType="DateTime" CanBeNull="True" />
         <Column Name="Freight" Type="System.Decimal"
            DbType="Money" CanBeNull="True" />
      </ElementType>
   </Function>
</Database>

Os elementos e seus atributos são descritos da seguinte maneira.

Banco de dados

Esse é o elemento mais externo no formato XML. Esse elemento é mapeado livremente para o atributo Database no DataContext gerado.

Atributos de banco de dados

Atributo Type Padrão Descrição
Nome String Nenhum O nome do banco de dados. Se estiver presente e se estiver gerando um DataContext, anexará um atributo De banco de dados a ele com esse nome. Também usado como nome da classe DataContext se o atributo de classe não estiver presente.
EntityNamespace Forte Nenhum Namespace padrão para classes geradas a partir de elementos Type em elementos Table. Se nenhum namespace for especificado aqui, as classes de entidade serão geradas no namespace raiz.
ContextNamespace String Nenhum Namespace padrão para a classe DataContext gerada. Se nenhum namespace for especificado aqui, a classe DataContext será gerada no namespace raiz.
Classe String Database.Name O nome da classe DataContext gerada. Se não estiver presente, use o atributo Name do elemento Database.
AccessModifier AccessModifier Público O nível de acessibilidade da classe DataContext gerada. Os valores válidos são Público, Protegido, Interno e Privado.
BaseType String "System.Data.Linq.DataContext" O tipo base da classe DataContext .
Provedor String "System.Data.Linq.SqlClient.Sql2005Provider" O provedor do DataContext, use o provedor Sql2005 como padrão
ExternalMapping Boolean Falso Especifique se o DBML é usado para gerar um arquivo de mapeamento externo.
Serialização SerializationMode SerializationMode.None Especifique se as classes DataContext e entity geradas são serializáveis.

Atributos de Sub-Element de banco de dados

Sub-Element Tipo de elemento Intervalo de ocorrências Descrição
<Table> Tabela 0-unbounded Representa uma tabela ou exibição SQL Server que será mapeada para um único tipo ou para uma hierarquia de herança.
<Função> Função 0-unbounded Representa um procedimento armazenado SQL Server ou uma função db que será mapeada para um método na classe DataContext gerada.
<Conexão> Conexão 0-1 Representa a conexão de banco de dados que este DataContext usará.

Tabela

Esse elemento representa uma tabela de banco de dados (ou uma exibição) que será mapeada para um único tipo ou para uma hierarquia de herança. Esse elemento é mapeado livremente para o atributo Table na classe de entidade gerada.

Atributos de tabela

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome da tabela dentro do banco de dados. Serve como a base do nome padrão para o adaptador de tabela, se necessário.
Membro String Table.Name O nome do campo membro gerado para essa tabela dentro da classe DataContext .
AccessModifier AccessModifier Público O nível de acessibilidade da referência da Tabela<T> no DataContext. Os valores válidos são Público, Protegido, Interno e Privado.

Atributos de Sub-Element de tabela

Sub-Element Tipo de elemento Intervalo de ocorrências Descrição
<Tipo> Type 1-1 Representa o tipo ou a hierarquia de herança mapeada para esta tabela.
<InsertFunction> TableFunction 0-1 O método para inserção. Quando ele está presente, um método InsertT é gerado.
<UpdateFunction> TableFunction 0-1 O método para atualização. Quando ele está presente, um método UpdateT é gerado.
<DeleteFunction> TableFunction 0-1 O método para exclusão. Quando ele está presente, um método DeleteT é gerado.

Type

Esse elemento representa uma definição de tipo para uma forma de resultado de procedimento armazenado ou Table. Isso gerará código em um novo tipo CLR com as colunas e associações especificadas.

O tipo também pode representar um componente de uma hierarquia de herança, com vários tipos mapeando para a mesma tabela. Nesse caso, os elementos Type são aninhados para representar as relações de herança pai-filho e são diferenciados no banco de dados pelo InheritanceCode especificado.

Atributos de tipo

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome do tipo CLR a ser gerado.
InheritanceCode String Nenhum Se esse tipo estiver participando da herança, ele poderá ter um código de herança associado para distinguir entre tipos CLR ao carregar linhas da tabela. O Tipo cujo InheritanceCode corresponde ao valor da coluna IsDiscriminator é usado para instanciar o objeto carregado. Se o código de herança não estiver presente, a classe de entidade gerada será abstrata.
IsInheritanceDefault Boolean Falso Se isso for verdadeiro para um Tipo em uma hierarquia de herança, esse tipo será usado ao carregar linhas que não correspondem a nenhum código de herança definido.
AccessModifier AccessModifier Público O nível de acessibilidade do tipo CLR que está sendo criado. Os valores válidos são: Público, Protegido, Interno e Privado.
Id String Nenhum Um tipo pode ter uma ID exclusiva. A ID de um tipo pode ser usada por outras tabelas ou funções. A ID aparece apenas no arquivo DBML, não no modelo de objeto.
Idref String Nenhum IdRef é usado para se referir à ID de outro tipo. Se IdRef estiver presente em um elemento de tipo, o elemento type deverá conter apenas as informações de IdRef . IdRef aparece apenas no arquivo DBML, não no modelo de objeto.

Atributos de Sub-Element de tipo

Sub-Element Tipo de elemento Intervalo de ocorrências Descrição
<Coluna> Coluna 0-unbounded Representa uma propriedade dentro desse tipo que será associada a um campo na tabela desse tipo.
<Associação> Associação 0-unbounded Representa uma propriedade dentro desse tipo que será associada a uma extremidade de uma relação de chave estrangeira entre tabelas.
<Tipo> SubType 0-unbounded Representa subtipos desse tipo dentro de uma hierarquia de herança.

SubType

Esse elemento representa um tipo derivado em uma hierarquia de herança. Isso será gerado em um novo tipo CLR com as colunas e associações especificadas nesse tipo. Nenhum atributo de herança é gerado para subtipos.

Comparando com Type, os elementos SubType não têm AccessModifier porque todos os tipos derivados devem ser públicos. SubTipos não podem ser reutilizados por outras tabelas e funções, portanto, não há nenhuma ID e IdRef nelas.

Atributos SubType

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome do tipo CLR a ser gerado.
InheritanceCode String Nenhum Se esse tipo estiver participando da herança, ele poderá ter um código de herança associado para distinguir entre tipos CLR ao carregar linhas da tabela. O Tipo cujo InheritanceCode corresponde ao valor da coluna IsDiscriminator é usado para instanciar o objeto carregado. Se o código de herança não estiver presente, a classe de entidade gerada será abstrata.
IsInheritanceDefault Boolean Falso Se isso for verdadeiro para um Tipo em uma hierarquia de herança, esse tipo será usado ao carregar linhas que não correspondem a nenhum código de herança definido.

Atributos de Sub-Element SubType

Sub-Element Tipo de elemento Intervalo de ocorrências Descrição
<Coluna> Coluna 0-unbounded Representa uma propriedade dentro desse tipo que será associada a um campo na tabela desse tipo.
<Associação> Associação 0-unbounded Representa uma propriedade dentro desse tipo que será associada a uma extremidade de uma relação de chave estrangeira entre tabelas.
<Tipo> SubType 0-unbounded Representa subtipos desse tipo dentro de uma hierarquia de herança.

Coluna

Esse elemento representa uma coluna dentro de uma tabela que é mapeada para uma propriedade (e campo de backup) dentro de uma classe. Não haverá nenhum elemento Column presente para qualquer extremidade de uma relação de chave estrangeira, no entanto, pois isso é completamente representado (em ambas as extremidades) pelos elementos Association.

Atributos de coluna

Atributos Tipo Padrão Descrição
Nome String Nenhum O nome do campo de banco de dados para o qual esta coluna será mapeada.
Membro String Nome O nome da propriedade CLR a ser gerada no tipo que contém.
Armazenamento String _Membro O nome do campo de suporte clr privado que armazenará o valor dessa coluna. Não remova o Armazenamento ao serializar, mesmo que ele seja padrão.
AccessModifier AccessModifier Público O nível de acessibilidade da propriedade CLR que está sendo criada. Os valores válidos são: Público, Protegido, Interno e Privado.
Type String (obrigatório) O nome do tipo da propriedade CLR e do campo de suporte que está sendo criado. Isso pode ser qualquer coisa, desde um nome totalmente qualificado até apenas o nome direto de uma classe, desde que o nome esteja no escopo quando o código gerado for compilado.
DbType String Nenhum Tipo de SQL Server completo (incluindo anotação como NOT NULL) para esta coluna. Usado por LINQ to SQL se você for fornecê-lo para otimizar as consultas geradas e ser mais específico ao fazer CreateDatabase(). Sempre serialize DbType.
IsReadOnly Boolean Falso Se IsReadOnly estiver definido, um setter de propriedade não será criado, o que significa que as pessoas não poderão alterar o valor dessa coluna usando esse objeto.
IsPrimaryKey Boolean Falso Indica que esta coluna participa da chave primária da tabela. Essas informações são necessárias para que LINQ to SQL operem corretamente.
Isdbgenerated Boolean Falso Indica que os dados desse campo são gerados pelo banco de dados. Esse é o caso principalmente para campos de Numeração Automática e para campos calculados. Não é significativo atribuir valores a esses campos e, portanto, eles são automaticamente IsReadOnly.
CanBeNull Boolean Nenhum Indica que o valor pode conter o valor nulo. Se você quiser realmente usar valores nulos no CLR, ainda deverá especificar o ClrType como T> anulável<.
UpdateCheck UpdateCheck Sempre (a menos que pelo menos um outro membro tenha IsVersion definido, em seguida, Nunca) Indica se LINQ to SQL deve usar essa coluna durante a detecção otimista de conflitos de simultaneidade. Normalmente, todas as colunas participam por padrão, a menos que haja uma coluna IsVersion , que participa por si só. Pode ser: Always, Never ou WhenChanged (o que significa que a coluna participa se seu próprio valor tiver sido alterado).
IsDiscriminator Boolean Falso Indica se esse campo contém o código discriminatório usado para escolher entre tipos em uma hierarquia de herança.
Expression String Nenhum Não afeta a operação do LINQ to SQL, mas é usado durante .CreateDatabase() como uma expressão SQL bruta que representa a expressão de coluna computada.
Isversion Boolean Falso Indica que esse campo representa um campo TIMESTAMP em SQL Server que é atualizado automaticamente sempre que a linha é alterada. Esse campo pode ser usado para habilitar a detecção de conflitos de simultaneidade otimista mais eficiente.
IsDelayLoaded Boolean Falso Indica que essa coluna não deve ser carregada imediatamente após a materialização do objeto, mas apenas quando a propriedade relevante é acessada pela primeira vez. Isso é útil para campos de memorando grandes ou dados binários em uma linha que nem sempre é necessária.
AutoSync AutoSync Se (IsDbGenerated && IsPrimaryKey) OnInsert;

Caso contrário, se (IsDbGenerated) Always

Else Never

Especifica se a coluna é sincronizada automaticamente do valor gerado pelo banco de dados. Os valores válidos para essa marca são: OnInsert, Always e Never.

Associação

Esse elemento representa uma das extremidades de uma relação de chave estrangeira. Para relações um-para-muitos, este será um EntitySet<T> de um lado e um EntityRef<T> no lado muitos. Para relações um-para-um, este será um EntityRef<T> em ambos os lados.

Observe que não é necessário ter uma entrada de Associação em ambos os lados de uma associação. Nesse caso, uma propriedade só será gerada no lado que tem a entrada (formando uma relação unidirecional).

Atributos de associação

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome da relação (geralmente o nome da restrição de chave estrangeira). Tecnicamente, isso pode ser opcional, mas sempre deve ser gerado por código para evitar ambiguidade quando houver várias relações entre as mesmas duas tabelas.
Membro String Nome O nome da propriedade CLR a ser gerada nesse lado da associação.
Armazenamento String Se OneToMany e Not IsForeignKey:

_OtherTable

Caso contrário:

_TypeName(OtherTable)

O nome do campo de suporte clr privado que armazenará o valor dessa coluna.
AccessModifier AccessModifier Público O nível de acessibilidade da propriedade CLR que está sendo criada. Os valores válidos são Público, Protegido, Interno e Privado.
Thiskey String A propriedade IsIdentity dentro da classe que contém Uma lista separada por vírgulas das chaves desse lado da associação.
OtherTable String Confira a descrição. A tabela do outro lado da relação. Normalmente, isso pode ser determinado pelo runtime LINQ to SQL por nomes de relação correspondentes, mas isso não é possível para associações unidirecionais ou associações anônimas.
Otherkey String As chaves primárias dentro da classe estrangeira Uma lista separada por vírgulas das chaves do outro lado da associação.
IsForeignKey Boolean Falso Indica se esse é o lado "filho" da relação, o lado muitos de um para muitos.
RelationshipType RelationshipType OneToMany Indica se o usuário está afirmando que os dados relacionados a essa associação atendem aos critérios de dados um para um ou se ajustam ao caso mais geral de um para muitos. Para um para um, o usuário está afirmando que para cada linha no lado da chave primária ("um"), há apenas uma linha no lado de chave estrangeira ("muitos"). Isso fará com que um EntityRef<T> seja gerado no lado "um" em vez de um EntitySet<T>. Os valores válidos são OneToOne e OneToMany.
Deleterule String Nenhum Usado para adicionar comportamento de exclusão a essa associação. Por exemplo, "CASCADE" adicionaria "ONDELETECASCADE" à relação FK. Se definido como nulo, nenhum comportamento de exclusão será adicionado.

Função

Esse elemento representa um procedimento armazenado ou uma função de banco de dados. Para cada nó de função , um método é gerado na classe DataContext .

Atributos de função

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome do procedimento armazenado no banco de dados.
Método String Método O nome do método CLR a ser gerado que permite a invocação do procedimento armazenado. O nome padrão para Método tem itens como [dbo]. removido Nome.
AccessModifier AccessModifier Público O nível de acessibilidade do método de procedimento armazenado. Os valores válidos são Público, Protegido, Interno e Privado.
HasMultipleResults Boolean Nº dos Tipos > 1 Especifica se o procedimento armazenado representado por este nó de função retorna vários conjuntos de resultados. Cada conjunto de resultados é uma forma tabular, pode ser um Tipo existente ou ser um conjunto de colunas. No último caso, um nó Tipo será criado para o conjunto de colunas.
IsComposable Boolean Falso Especifica se a função/procedimento armazenado pode ser composto em consultas LINQ to SQL. Somente funções de BD que não retornam void podem ser compostas.

Atributos de Sub-Element de função

Sub-Element Tipos de elemento Intervalo de ocorrências Descrição
<Parâmetro> Parâmetro 0-unbounded Representa os parâmetros de entrada e saída desse procedimento armazenado.
<Elementtype> Type 0-unbounded Representa as formas tabulares que o procedimento armazenado correspondente pode retornar.
<Return> Retorno 0-1 O tipo escalar retornado dessa função db ou procedimento armazenado. Se Return for nulo, a função retornará void. Uma função não pode ter Return e ElementType.

TableFunction

Esse elemento representa funções de substituição cud para tabelas. O designer LINQ to SQL permite a criação de métodos de substituição Inserir, Atualizar e Excluir para LINQ TO SQL e permite o mapeamento de nomes de propriedade de entidade para nomes de parâmetro de procedimento armazenado.

O nome do método para funções CUD é corrigido para que não haja nenhum atributo Method em DBML para elementos TableFunction . Por exemplo, para a tabela Cliente, os métodos CUD são nomeados como InsertCustomer, UpdateCustomer e DeleteCustomer.

Uma função de tabela não pode retornar a forma tabular, portanto, não há nenhum atributo ElementType no elemento TableFunction .

Atributos TableFunction

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome do procedimento armazenado no banco de dados.
AccessModifier AccessModifier Privado O nível de acessibilidade do método de procedimento armazenado. Os valores válidos são Público, Protegido, Interno e Privado.
HasMultipleResults Boolean Nº dos Tipos > 1 Especifica se o procedimento armazenado representado por este nó de função retorna vários conjuntos de resultados. Cada conjunto de resultados é uma forma tabular, pode ser um Tipo existente ou ser um conjunto de colunas. No último caso, um nó Tipo será criado para o conjunto de colunas.
IsComposable Boolean Falso Especifica se a função/procedimento armazenado pode ser composto em consultas LINQ to SQL. Somente funções de BD que não retornam void podem ser compostas.

Atributos de Sub-Element TableFunction

Sub-Elements Tipo de elemento Intervalo de ocorrências Descrição
<Parâmetro> TableFunctionParameter 0-unbounded Representa os parâmetros de entrada e saída dessa função de tabela.
<Return> TableFunctionReturn 0-1 O tipo escalar retornado dessa função de tabela. Se Return for nulo, a função retornará void.

Parâmetro

Esse elemento representa um parâmetro de procedimento/função armazenado. Os parâmetros podem passar dados para dentro e para fora.

Atributos de parâmetro

Atributo Type Padrão Descrições
Nome String (obrigatório) O nome do banco de dados do parâmetro proc/function armazenado.
Parâmetro String Nome O nome CLR do parâmetro de método.
  String (obrigatório) O nome CLR do parâmetro de método.
DbType String Nenhum O tipo de BD do parâmetro proc/function armazenado.
Direção ParameterDirection In A direção que o parâmetro flui. Pode ser um de Entrada, Saída e InOut.

Retorno

Esse elemento representa o tipo de retorno de um procedimento/função armazenado.

Atributos de retorno

Atributo Type Padrão Descrição
Type String (obrigatório) O tipo CLR do resultado do proc/função armazenado.
DbType String Nenhum O tipo de BD do resultado do proc/função armazenado.

TableFunctionParameter

Esse elemento representa um parâmetro de uma função CUD. Os parâmetros podem passar dados para dentro e para fora. Cada parâmetro é mapeado para uma coluna Table à qual essa função CUD pertence. Não há atributos Type ou DbType nesse elemento porque as informações de tipo podem ser obtidas da coluna para a qual o parâmetro é mapeado.

Atributos TableFunctionParameter

Atributo Type Padrão Descrição
Nome String (obrigatório) O nome do banco de dados do parâmetro de função CUD.
Parâmetro String Nome O nome CLR do parâmetro de método.
Coluna String Nome O nome da coluna para o qual esse parâmetro está mapeando.
Direção ParameterDirection In A direção que o parâmetro flui. Pode ser um de Entrada, Saída ou InOut.
Versão Versão Current Se PropertyName está se referindo à versão atual ou original de uma determinada coluna. Aplicável somente durante a substituição de atualização . Pode ser Atual ou Original.

TableFunctionReturn

Esse elemento representa um tipo de retorno de uma função CUD. Na verdade, ele contém apenas o nome da coluna mapeado para o resultado da função CUD. As informações de tipo do retorno podem ser obtidas da coluna.

Atributo TableFunctionReturn

Attrobite Type Padrão Descrição
Coluna String Nenhum O nome da coluna para o qual o retorno está mapeando.

Conexão

Esse elemento representa parâmetros de conexão de banco de dados padrão. Isso permite a criação de um construtor padrão para o tipo DataContext que já sabe como se conectar a um banco de dados.

Há dois tipos de conexões padrão possíveis, um com ConnectionString direto e outro que lê app.settings.

Atributos de conexão

Atributo Type Padrão Descrição
UseApplicationSettings Boolean Falso Determina se um arquivo App.Settings deve ser usado ou obter configurações de aplicativo de um ConnectionString direto.
ConnectionString String Nenhum A cadeia de conexão a ser enviada para o provedor de dados SQL.
SettingsObjectName String Configurações O Objeto App.Settings do qual recuperar as propriedades.
SettingsPropertyName String ConnectionString A propriedade App.Settings que contém ConnectionString.

Entidades de várias camadas

Em aplicativos de duas camadas, um único DataContext manipula consultas e atualizações. No entanto, para aplicativos com camadas adicionais, geralmente é necessário usar instâncias separadas do DataContext para consultas e atualizações. Por exemplo, no caso de aplicativos ASP.NET, a consulta e a atualização são feitas para solicitações separadas para o servidor Web. Portanto, é impraticável usar a mesma instância DataContext em várias solicitações. Nesses casos, uma instância do DataContext precisa ser capaz de atualizar objetos que não foi recuperado. O suporte à entidade de várias camadas no LINQ to SQL fornece essa funcionalidade por meio do método Attach().

Aqui está um exemplo de como um objeto Customer pode ser alterado usando uma instância de DataContext diferente:

C#

// Customer entity changed on another tier – for example, through a browser
// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);

// Create a new entity for applying changes
Customer C2 = new Customer();
C2.CustomerID ="NewCustID";

// Set other properties needed for optimistic concurrency check
C2.CompanyName = "New Company Name Co.";

...

// Tell LINQ to SQL to track this object for an update; that is, not for insertion
db2.Customers.Attach(C2);

// Now apply the changes
C2.ContactName = "Mary Anders";

// DataContext now knows how to update the customer
db2.SubmitChanges();

Visual Basic

' Customer entity changed on another tier – for example, through a browser
' Back on the mid-tier, a new context needs to be used
Dim db2 As Northwind = New Northwind(…)

' Create a new entity for applying changes
Dim C2 As New Customer()
C2.CustomerID =”NewCustID”

' Set other properties needed for optimistic concurrency check
C2.CompanyName = ”New Company Name Co.”

...

' Tell LINQ to SQL to track this object for an update; that is, not for insertion
db2.Customers.Attach(C2)

' Now apply the changes
C2.ContactName = "Mary Anders"

' DataContext now knows how to update the customer
db2.SubmitChanges()

Em aplicativos de várias camadas, a entidade inteira geralmente não é enviada entre camadas para simplicidade, interoperabilidade ou privacidade. Por exemplo, um fornecedor pode definir um contrato de dados para um serviço Web diferente da entidade Order usada na camada intermediária. Da mesma forma, uma página da Web pode mostrar apenas um subconjunto dos membros de uma entidade Employee. Portanto, o suporte de várias camadas foi projetado para acomodar esses casos. Somente os membros pertencentes a uma ou mais das categorias a seguir precisam ser transportados entre camadas e definidos antes de chamar Attach().

  1. Membros que fazem parte da identidade da entidade.
  2. Membros que foram alterados.
  3. Membros que participam de marcar de simultaneidade otimistas.

Se um carimbo de data/hora ou uma coluna de número de versão for usado para marcar de simultaneidade otimista, o membro correspondente deverá ser definido antes de chamar Attach(). Os valores para outros membros não precisam ser definidos antes de chamar Attach(). LINQ to SQL usa atualizações mínimas com verificações de simultaneidade otimistas; ou seja, um membro que não está definido ou verificado quanto à simultaneidade otimista é ignorado.

Os valores originais necessários para verificações de simultaneidade otimistas podem ser mantidos usando uma variedade de mecanismos fora do escopo de APIs de LINQ to SQL. Um aplicativo ASP.NET pode usar um estado de exibição (ou um controle que usa o estado de exibição). Um serviço Web pode usar o DataContract para um método de atualização para garantir que os valores originais estejam disponíveis para processamento de atualização. No interesse da interoperabilidade e da generalidade, LINQ to SQL não dita a forma dos dados trocados entre camadas ou os mecanismos usados para arredondar os valores originais.

Entidades para inserção e exclusão não exigem o método Attach(). Os métodos usados para aplicativos de duas camadas — Table.Add() e Table.Remove() podem ser usados para inserção e exclusão. Como no caso de atualizações de duas camadas, um usuário é responsável por lidar com restrições de chave estrangeira. Um cliente com pedidos não pode ser removido sem lidar com seus pedidos se houver uma restrição de chave estrangeira no banco de dados impedindo a exclusão de um cliente com pedidos.

LINQ to SQL também manipula o anexo de entidades para atualizações transitivamente. O usuário essencialmente cria o grafo de objeto de pré-atualização conforme desejado e chama Attach(). Todas as alterações podem ser "reproduzidas" no grafo anexado para realizar as atualizações necessárias, conforme mostrado abaixo:

C#

Northwind db1 = new Northwind(…);
// Assume Customer c1 and related Orders o1, o2 are retrieved

// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);

// Create new entities for applying changes
Customer c2 = new Customer();
c2.CustomerID = c.CustomerID;
Order o2 = new Order();
o2.OrderID = ...;

c2.Orders.Add(o2);

// Add other related objects needed for updates

// Set properties needed for optimistic concurrency check
...
// Order o1 to be deleted
Order o1 = new Order();
o1.OrderID = ...;

// Tell LINQ to SQL to track the graph transitively
db2.Customers.Attach(c2);
// Now "replay" all the changes

// Updates
c2.ContactName = ...;
o2.ShipAddress = ...;

// New object for insertion
Order o3 = new Order();
o3.OrderID = ...;
c2.Orders.Add(o3);

// Remove order o1
db2.Orders.Remove(o1);

// DataContext now knows how to do update/insert/delete
db2.SubmitChanges();

Visual Basic

Dim db1 As Northwind = New Northwind(…)
' Assume Customer c1 and related Orders o1, o2 are retrieved

' Back on the mid-tier, a new context needs to be used
Dim db2 As Northwind = New Northwind(…)

' Create new entities for applying changes
Customer c2 = new Customer()
c2.CustomerID = c.CustomerID
Dim o2 As Order = New Order()
o2.OrderID = ...

c2.Orders.Add(o2)

' Add other related objects needed for updates

' Set properties needed for optimistic concurrency check
...
' Order o1 to be deleted
Dim o1 As Order = New Order()
o1.OrderID = ...

' Tell LINQ to SQL to track the graph transitively
db2.Customers.Attach(c2)
' Now "replay" all the changes

' Updates
c2.ContactName = ...
o2.ShipAddress = ...

' New object for insertion
Dim o3 As Order = New Order()
o3.OrderID = ...
c2.Orders.Add(o3)

' Remove order o1
db2.Orders.Remove(o1)

' DataContext now knows how to do update/insert/delete
db2.SubmitChanges()

Mapeamento externo

Além do mapeamento baseado em atributo, LINQ to SQL também dá suporte ao mapeamento externo. A forma mais comum de mapeamento externo é um arquivo XML. Os arquivos de mapeamento habilitam cenários adicionais em que a separação do mapeamento do código é desejável.

O DataContext fornece um construtor adicional para fornecer um MappingSource. Uma forma de MappingSource é um XmlMappingSource que pode ser construído a partir de um arquivo de mapeamento XML.

Aqui está um exemplo de como o arquivo de mapeamento pode ser usado:

C#

String path = @"C:\Mapping\NorthwindMapping.xml";
XmlMappingSource prodMapping = 
   XmlMappingSource.FromXml(File.ReadAllText(path));
Northwind db = new Northwind(
   @"Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf",
   prodMapping
   );

Visual Basic

Dim path As String = "C:\Mapping\NorthwindMapping.xml"
Dim prodMapping As XmlMappingSource = _
   XmlMappingSource.FromXml(File.ReadAllText(path))
Dim db As Northwind = New Northwind( _
   "Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf", _
   prodMapping   )

Aqui está um snippet correspondente do arquivo de mapeamento mostrando o mapeamento para a classe Product . Ele mostra a classe Produto no namespace Mapeamento mapeado para a tabela Produtos no banco de dados Northwind . Os elementos e atributos são consistentes com os nomes de atributo e os parâmetros.

<?xml version="1.0" encoding="utf-8"?>
<Database xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
   xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="Northwind"
   ProviderType="System.Data.Linq.SqlClient.Sql2005Provider">
   <Table Name="Products">
      <Type Name="Mappings.FunctionMapping.Product">
         <Column Name="ProductID" Member="ProductID" Storage="_ProductID"
            DbType="Int NOT NULL IDENTITY" IsPrimaryKey="True"
            IsDBGenerated="True" AutoSync="OnInsert" />
         <Column Name="ProductName" Member="ProductName" Storage="_ProductName"
            DbType="NVarChar(40) NOT NULL" CanBeNull="False" />
         <Column Name="SupplierID" Member="SupplierID" Storage="_SupplierID"
            DbType="Int" />
         <Column Name="CategoryID" Member="CategoryID" Storage="_CategoryID"
            DbType="Int" />
         <Column Name="QuantityPerUnit" Member="QuantityPerUnit"
            Storage="_QuantityPerUnit" DbType="NVarChar(20)" />
         <Column Name="UnitPrice" Member="UnitPrice" Storage="_UnitPrice"
            DbType="Money" />
         <Column Name="UnitsInStock" Member="UnitsInStock" Storage="_UnitsInStock"
            DbType="SmallInt" />
         <Column Name="UnitsOnOrder" Member="UnitsOnOrder" Storage="_UnitsOnOrder"
            DbType="SmallInt" />
         <Column Name="ReorderLevel" Member="ReorderLevel" Storage="_ReorderLevel"
            DbType="SmallInt" />
         <Column Name="Discontinued" Member="Discontinued" Storage="_Discontinued"
            DbType="Bit NOT NULL" />
         <Association Name="FK_Order_Details_Products" Member="OrderDetails"
            Storage="_OrderDetails" ThisKey="ProductID" OtherTable="Order Details"
            OtherKey="ProductID" DeleteRule="NO ACTION" />
         <Association Name="FK_Products_Categories" Member="Category"
            Storage="_Category" ThisKey="CategoryID" OtherTable="Categories"
            OtherKey="CategoryID" IsForeignKey="True" />
         <Association Name="FK_Products_Suppliers" Member="Supplier"
            Storage="_Supplier" ThisKey="SupplierID" OtherTable="Suppliers"
            OtherKey="SupplierID" IsForeignKey="True" />
      </Type>
   </Table>

</Database>

Notas e suporte a funções do NET Framework

Os parágrafos a seguir fornecem informações básicas sobre LINQ to SQL suporte de tipo e diferenças em relação ao .NET Framework.

Tipos primitivos

Implemented

  • Operadores aritméticos e de comparação
  • Operadores shift: << e >>
  • A conversão entre char e numeric é feita pelo NCHAR UNICODE/; caso contrário, o CONVERT do SQL é usado.

Não implementado

  • <Digite>. Analisar
  • Enumerações podem ser usadas e mapeadas para inteiros e cadeias de caracteres em uma tabela. Para este último, os métodos Parse e ToString() são usados.

Diferença do .NET

  • A saída de ToString para uso duplo usa CONVERT(NVARCHAR(30), @x, 2) no SQL, que sempre usa 16 dígitos e "Notação Científica". Por exemplo: "0,000000000000000e+000" para 0, portanto, ele não dá a mesma cadeia de caracteres que . Convert.ToString()do NET.

System.String

Implemented

  • Métodos não estáticos:

    • Length, Substring, Contains, StartsWith, EndsWith, IndexOf, Insert, Remove, Replace, Trim, ToLower, ToUpper, LastIndexOf, PadRight, PadLeft, Equals, CompareTo. Todas as assinaturas têm suporte, exceto quando assumem o parâmetro StringComparison e assim por diante, conforme detalhado abaixo.
  • Métodos estáticos:

       Concat(...)               all signatures
       Compare(String, String)
       String (indexer) 
       Equals(String, String)
    
  • Construtor:

        String(Char, Int32)
    
  • Operadores:

      +, ==, != (+, =, and <> in Visual Basic)
    

Não implementado

  • Métodos que tomam ou produzem uma matriz de char.

  • Métodos que levam um CultureInfo/StringComparison/IFormatProvider.

  • Estático (compartilhado no Visual Basic):

       Copy(String str)
       Compare(String, String, Boolean)
       Compare(String, String, StringComparison)
       Compare(String, String, Boolean, CultureInfo) 
       Compare(String, Int32, String, Int32, Int32)
       Compare(String, Int32, String, Int32, Int32,   Boolean)
       Compare(String, Int32, String, Int32, Int32, StringComparison)
       Compare(String, Int32, String, Int32, Int32, Boolean, CultureInfo)
       CompareOrdinal(String, String)
       CompareOrdinal(String, Int32, String, Int32, Int32)
       Join(String, ArrayOf String [,...]) All Join version with first three args
    
  • Instância:

       ToUpperInvariant()
       Format(String, Object)      + overloads
       IndexOf(String, Int32, StringComparison)
       IndexOfAny(ArrayOf Char)
       Normalize()
       Normalize(NormalizationForm)
       IsNormalized()
       Split(...)
       StartsWith(String, StringComparison)
       ToCharArray()
       ToUpper(CultureInfo)
       TrimEnd(ParamArray Char)
       TrimStart(ParamArray Char)
    

Restrições/diferença do .NET

O SQL usa ordenações para determinar a igualdade e a ordenação de cadeias de caracteres. Eles podem ser especificados em uma Instância SQL Server, um banco de dados, uma coluna de tabela ou uma expressão.

As traduções das funções implementadas até o momento não alteram a ordenação nem especificam uma ordenação diferente nas expressões traduzidas. Portanto, se a ordenação padrão não diferencia maiúsculas de minúsculas, funções como CompareTo ou IndexOf podem fornecer resultados que diferem do que as funções .NET (diferenciam maiúsculas de minúsculas) dariam.

Os métodos StartsWith(str)/EndsWith(str) pressupõem que o argumento str seja uma constante ou uma expressão avaliada no cliente. Ou seja, atualmente não é possível usar uma coluna para str.

System.Math

Métodos estáticos implementados

  • Todas as assinaturas:
    • Abs, Acos, Asin, Atan, Atan2, BigMul, Ceiling, Cos, Cosh, Exp, Floor, Log, Log10, Max, Min, Pow, Sign, Sinh, Sqrt, Tan, Tanh ou Truncate.

Não implementado

  • IEEERemainder.
  • DivRem tem um parâmetro out, portanto, você não pode usá-lo em uma expressão. As constantes Math.PI e Math.E são avaliadas no cliente, portanto, elas não precisam de uma tradução.

Diferença do .NET

A tradução da função .NET Math.Round é a função SQL ROUND. Há suporte para a tradução somente quando uma sobrecarga é especificada que indica o valor de enumeração MidpointRounding . MidpointRounding.AwayFromZero é o comportamento do SQL e MidpointRounding.ToEven indica o comportamento clr.

System.Convert

Implemented

  • Métodos de formulário Para<Type1>(<Type2> x) em que Type1, Type2 é um dos:
    • bool, byte, char, DateTime, decimal, double, float, Int16, Int32, Int64 ou string.
  • O comportamento é o mesmo que uma conversão:
    • Para ToString(Double), há um código especial para obter a precisão completa.
    • Para conversão Int32/Char, LINQ to SQL usa a função NCHAR UNICODE/ do SQL.
    • Caso contrário, a tradução será CONVERT.

Não implementado

  • ToSByte, UInt16, 32, 64: esses tipos não existem no SQL.

    To<integer type>(String, Int32) 
    ToString(..., Int32)       any overload ending with an Int32 toBase
    IsDBNull(Object)
    GetTypeCode(Object)
    ChangeType(...)
    
  • Versões com o parâmetro IFormatProvider .

  • Métodos que envolvem uma matriz (To/FromBase64CharArray, To/FromBase64String).

System.TimeSpan

Implemented

  • Construtores:

       TimeSpan(Long)
       TimeSpan (year, month, day)
       TimeSpan (year, month, day, hour, minutes, seconds)
       TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
    
  • Operadores:

       Comparison operators: <,==, and so on in C#; <, =, and so on in Visual Basic
    
       +, -
    
  • Métodos estáticos (compartilhados no Visual Basic):

       Compare(t1,t2)
    
  • Métodos/propriedades não estáticos (Instância):

       Ticks, Milliseconds, Seconds, Hours, Days
       TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays,
       Equals, CompareTo(TimeSpan)
       Add(TimeSpan), Subtract(TimeSpan)
       Duration() [= ABS], Negate()
    

Não implementado

   ToString()
   TimeSpan FromDay(Double), FromHours,   all From Variants
   TimeSpan Parse(String)

System.DateTime

Implemented

  • Construtores:

       DateTime(year, month, day)
       DateTime(year, month, day, hour, minutes, seconds)
       DateTime(year, month, day, hour, minutes, seconds, milliseconds)
    
  • Operadores:

       Comparisons
       DateTime – DateTime (gives TimeSpan)
       DateTime + TimeSpan (gives DateTime)
       DateTime – TimeSpan (gives DateTime)
    
  • Métodos estáticos (compartilhados):

       Add(TimeSpan), AddTicks(Long),
       AddDays/Hours/Milliseconds/Minutes (Double)
       AddMonths/Years(Int32)
       Equals
    
  • Métodos/propriedades não estáticos (Instância):

       Day, Month, Year, Hour, Minute, Second, Millisecond, DayOfWeek
       CompareTo(DateTime)
       TimeOfDay()
       Equals
       ToString()
    

Diferença do .NET

Os valores de datetime do SQL são arredondados para 0,000, 0,003 ou 0,007 segundos, portanto, é menos preciso do que os do .NET.

O intervalo de datetime do SQL começa em 1º de janeiro de 1753.

O SQL não tem um tipo interno para TimeSpan. Ele usa diferentes métodos DATEDIFF que retornam inteiros de 32 bits. Um deles é DATEDIFF(DAY,...), que fornece o número de dias; outro é DATEDIFF(MILLISECOND,...), que fornece o número de milissegundos. Um erro resultará se DateTimes tiver mais de 24 dias de diferença. Por outro lado, o .NET usa inteiros de 64 bits e mede TimeSpans em tiques.

Para chegar o mais próximo possível da semântica do .NET no SQL, LINQ to SQL converte TimeSpans em inteiros de 64 bits e usa os dois métodos DATEDIFF mencionados acima para calcular o número de tiques entre duas datas.

Datetime UtcNow é avaliado no cliente quando a consulta é traduzida (como qualquer expressão que não envolva dados de banco de dados).

Não implementado

   IsDaylightSavingTime()
   IsLeapYear(Int32)
   DaysInMonth(Int32, Int32)
   ToBinary()
   ToFileTime()
   ToFileTimeUtc()
   ToLongDateString()
   ToLongTimeString()
   ToOADate()
   ToShortDateString()
   ToShortTimeString()
   ToUniversalTime()
   FromBinary(Long), FileTime, FileTimeUtc, OADate
   GetDateTimeFormats(...)
   constructor DateTime(Long)
   Parse(String)
   DayOfYear

Depuração de suporte

O DataContext fornece métodos e propriedades para obter o SQL gerado para consultas e processamento de alterações. Esses métodos podem ser úteis para entender LINQ to SQL funcionalidade e para depurar problemas específicos.

Métodos DataContext para obter o SQL gerado

Membro Finalidade
Registro Imprime o SQL antes de ser executado. Aborda comandos de consulta, inserção, atualização e exclusão. Uso:

C#

db.Log = Console.Out;

Visual Basic

db.Log = Console.Out

GetQueryText(query) Retorna o texto da consulta sem executá-la. Uso:

C#

Console.WriteLine(db.GetQueryText(db.Customers));

Visual Basic

Console.WriteLine(db.GetQueryTest(db.Customers))

GetChangeText() Retorna o texto dos comandos SQL para inserir/atualizar/excluir sem executá-los. Uso:

C#

Console.WriteLine(db.GetChangeText());

Visual Basic

Console.WriteLine(db.GetChangeText())