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 :
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.
É 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.
|
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().
- Membros que fazem parte da identidade da entidade.
- Membros que foram alterados.
- 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#
Visual Basic
|
GetQueryText(query) | Retorna o texto da consulta sem executá-la. Uso: C# Console.WriteLine(db.GetQueryText(db.Customers));
Visual Basic
|
GetChangeText() | Retorna o texto dos comandos SQL para inserir/atualizar/excluir sem executá-los. Uso: C# Console.WriteLine(db.GetChangeText());
Visual Basic
|