LINQ to SQL: 关系数据的 .NET 语言集成查询

 

迪内什·库尔卡尼、卢卡·博洛尼亚人、马特·沃伦、安德斯·赫尔斯伯格、基特·乔治

2007 年 3 月

适用于:
   Visual Studio Code名称“Orcas”
   .Net Framework 3.5

总结: LINQ to SQL 提供运行时基础结构,用于将关系数据作为对象进行管理,而不会失去查询功能。 当 LINQ to SQL 自动留在后台跟踪更改时,应用程序可以自由操作对象。 ) (119 个打印页

目录

简介
快速浏览
   创建实体类
   The DataContext
   定义关系
   跨关系查询
   修改和保存实体
查询In-Depth
   查询执行
   对象标识
   关系
   联接
   投影
   编译的查询
   SQL 翻译
实体生命周期
   跟踪更改
   提交更改
   同时更改
   事务
   存储过程
实体类In-Depth
   使用属性
   图形一致性
   更改通知
   继承
高级主题
   创建数据库
   与 ADO.NET 互操作
   更改冲突解决
   存储过程调用
   实体类生成器工具
   生成器工具 DBML 参考
   多层实体
   外部映射
   NET Framework 函数支持和说明
   调试支持

简介

目前编写的大多数程序都以这样或那样的方式操作数据,并且这些数据通常存储在关系数据库中。 然而,现代编程语言和数据库在如何表示和操作信息方面存在巨大分歧。 这种不匹配通过多种方式可见。 最值得注意的是,编程语言通过要求将查询指定为文本字符串的 API 访问数据库中的信息。 这些查询是程序逻辑的重要部分。 然而,它们与语言不透明,无法从编译时验证和设计时功能(如 IntelliSense)中获益。

当然,分歧远不止于此。 信息的表示方式(数据模型)在两者之间大不相同。 新式编程语言以对象的形式定义信息。 关系数据库使用行。 对象具有唯一标识,因为每个实例在物理上不同于另一个实例。 行由主键值标识。 对象具有标识实例并将其链接在一起的引用。 行是有意区分的,要求使用外键将相关行松散地绑在一起。 对象独立存在,只要它们仍被另一个对象引用。 行作为表的元素存在,一旦删除它们就会消失。

难怪希望弥合这一差距的应用程序难以构建和维护。 这肯定会简化等式,以摆脱一边或另一边。 然而,关系数据库为长期存储和查询处理提供了关键基础结构,而现代编程语言对于敏捷开发和丰富计算来说是必不可少的。

到目前为止,应用程序开发人员一直需要单独解决每个应用程序中的此不匹配问题。 迄今为止,最好的解决方案是复杂的数据库抽象层,这些层在应用程序特定于域的对象模型和数据库的表格表示形式之间传送信息,并采用各种方式重塑和重新设置数据格式。 然而,通过掩盖真正的数据源,这些解决方案最终丢弃了关系数据库最引人注目的功能:查询数据的功能。

LINQ to SQL 是Visual Studio Code名称“Orcas”的组件,它提供运行时基础结构,用于将关系数据作为对象进行管理,而不会失去查询功能。 它通过将语言集成查询转换为 SQL 供数据库执行,然后将表格结果转换回你定义的对象来执行此操作。 然后,应用程序可以自由操作对象,而 LINQ to SQL 则会自动在后台跟踪更改。

  • LINQ to SQL 设计为对应用程序不具有侵入性。
    • 可以将当前 ADO.NET 解决方案逐段迁移到 LINQ to SQL, (共享) 相同的连接和事务,因为 LINQ to SQL 只是 ADO.NET 系列中的另一个组件。 LINQ to SQL 还对存储过程提供广泛的支持,允许重用现有企业资产。
  • LINQ to SQL 应用程序易于入门。
    • 可以像定义普通对象一样定义链接到关系数据的对象,只使用属性修饰以标识属性与列的对应方式。 当然,甚至没有必要手动执行此操作。 提供了一个设计时工具,用于自动将预先存在的关系数据库架构转换为对象定义。

LINQ to SQL 运行时基础结构和设计时工具共同减少了数据库应用程序开发人员的工作负载。 以下各章概述了如何使用 LINQ to SQL 执行与数据库相关的常见任务。 假定读者熟悉Language-Integrated查询和标准查询运算符。

LINQ to SQL 与语言无关。 为提供Language-Integrated查询构建的任何语言都可以使用它来访问存储在关系数据库中的信息。 本文档中的示例以 C# 和 Visual Basic 显示;LINQ to SQL 也可以与启用了 LINQ 的 Visual Basic 编译器版本一起使用。

快速浏览

生成 LINQ to SQL 应用程序的第一步是声明将用于表示应用程序数据的对象类。 让我们演练一遍示例。

创建实体类

我们将从一个简单的类 Customer 开始,并将其与 Northwind 示例数据库中的 customers 表相关联。 为此,只需将自定义属性应用于类声明的顶部。 LINQ to SQL 为此定义 Table 属性。

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

Table 属性具有 Name 属性,可用于指定数据库表的确切名称。 如果未提供 Name 属性,LINQ to SQL 将假定数据库表与 类同名。 只有声明为表的类的实例才会存储在数据库中。 这些类型的类的实例称为 实体。 类本身称为 实体类

除了将类关联到表之外,还需要指示要与数据库列关联的每个字段或属性。 为此,LINQ to SQL 定义了 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

Column 属性具有各种属性,可用于自定义字段和数据库列之间的确切映射。 请注意的一个属性是 Id 属性。 它告知 LINQ to SQL 数据库列是表中主键的一部分。

Table 属性一样,仅当 Column 属性与字段或属性声明中可以推导的信息不同时,才需要提供该属性中的信息。 在此示例中,需要告知 LINQ to SQL CustomerID 字段是表中主键的一部分,但不必指定确切的名称或类型。

只有声明为列的字段和属性才会持久保存到数据库中或从数据库中检索。 其他部分将被视为应用程序逻辑的暂时性部分。

The DataContext

DataContext 是main管道,用于从数据库中检索对象并重新提交更改。 使用方式与使用 ADO.NET 连接的方式相同。 事实上, DataContext 是使用你提供的连接或连接字符串进行初始化的。 DataContext 的目的是将对象请求转换为针对数据库执行的 SQL 查询,然后从结果中组合对象。 DataContext 通过实现与标准查询运算符(例如 WhereSelect)相同的运算符模式来启用语言集成查询。

例如,可以使用 DataContext 检索其城市为伦敦的客户对象,如下所示:

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

每个数据库表表示为 Table 集合,可通过 GetTable () 方法使用其实体类进行标识。 建议声明强类型 DataContext ,而不是依赖于基本 DataContext 类和 GetTable () 方法。 强类型 DataContext 将所有 集合声明为上下文的成员。

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

然后,对来自伦敦客户的查询可以更简单地表示为:

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

我们将继续对概述文档的其余部分使用强类型 Northwind 类。

定义关系

关系数据库中的关系通常建模为引用其他表中主键的外键值。 若要在它们之间导航,必须使用关系联接操作将这两个表显式组合在一起。 另一方面,对象使用属性引用或使用“点”表示法导航的引用集合相互引用。 显然,点号比加入更简单,因为每次导航时都不需要回忆显式联接条件。

对于数据关系(例如这些关系始终相同),在实体类中将其编码为属性引用会非常方便。 LINQ to SQL 定义了一个 Association 属性,可应用于用于表示关系的成员。 关联关系类似于通过匹配表之间的列值建立的外键到主键关系。

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

Customer 类现在有一个属性,用于声明客户与其订单之间的关系。 Orders 属性的类型为 EntitySet,因为关系是一对多关系。 我们使用 Association 属性中的 OtherKey 属性来描述此关联的完成方式。 它指定要与此类进行比较的相关类中的属性的名称。 还有一个未指定的 ThisKey 属性。 通常,我们会使用它来列出关系这一端的成员。 但是,通过省略它,我们允许 LINQ to SQL 从构成主键的成员中推断它们。

请注意, 这在 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

Order 类使用 EntityRef 类型将关系描述回客户。 需要使用 EntityRef 类来支持 延迟加载 (稍后) 讨论。 Customer 属性的 Association 属性指定 ThisKey 属性,因为不可推断的成员现在位于关系的这一端。

另请查看 Storage 属性。 它告知 LINQ to SQL 哪个专用成员用于保存 属性的值。 这允许 LINQ to SQL 在存储和检索其值时绕过公共属性访问器。 如果希望 LINQ to SQL 避免将任何自定义业务逻辑写入访问器,这一点至关重要。 如果未指定存储属性,则将改用公共访问器。 还可以将 Storage 属性与 Column 属性一起使用。

在实体类中引入关系后,需要编写的代码量会随着你引入对通知和图形一致性的支持而增加。 幸运的是,有一个工具 (稍后) 介绍,可用于生成所有必需的定义作为分部类,使你能够混合使用生成的代码和自定义业务逻辑。

对于本文档的其余部分,我们假设工具已用于生成完整的 Northwind 数据上下文和所有实体类。

跨关系查询

有了关系后,只需引用类中定义的关系属性,即可在编写查询时使用这些关系。

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

上述查询使用 Orders 属性来形成客户和订单之间的交叉产品,从而生成新的 客户订单 对序列。

也可以反向执行操作。

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

在此示例中,将查询订单,并使用 Customer 关系访问有关关联的 Customer 对象的信息。

修改和保存实体

很少有应用程序是在构建时只考虑查询的。 还必须创建和修改数据。 LINQ to SQL 旨在提供在操作和持久保存对对象所做的更改方面的最大灵活性。 一旦实体对象可用(通过查询检索它们或重新构造它们),就可以将它们作为应用程序中的普通对象进行处理,更改其值或根据需要从集合中添加和删除它们。 LINQ to SQL 跟踪所有更改,并准备好在完成后立即将其传输回数据库。

以下示例使用工具从整个 Northwind 示例数据库的元数据生成的 CustomerOrder 类。 为了简洁起见,尚未显示类定义。

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()

调用 SubmitChanges () 时,LINQ to SQL 会自动生成并执行 SQL 命令,以便将更改传输回数据库。 还可以使用自定义逻辑替代此行为。 自定义逻辑可以调用数据库存储过程。

查询In-Depth

LINQ to SQL 为与关系数据库中的表关联的对象提供标准查询运算符的实现。 本章介绍特定于 LINQ to SQL 的查询方面。

查询执行

无论是将查询编写为高级 查询表达式 ,还是从单个运算符中生成查询,您编写的查询都不是立即执行的命令性语句。 它是一个说明。 例如,在下面的声明中,局部变量 q 引用查询的说明,而不是执行查询的结果。

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

此实例中 q 的实际类型为 IQueryable<Customer>。 直到应用程序尝试枚举它实际执行的查询的内容。 在此示例中, foreach 语句会导致执行。

IQueryable 对象类似于 ADO.NET 命令对象。 同时拥有一个查询并不意味着查询已执行。 命令对象保留描述查询的字符串。 同样, IQueryable 对象保留编码为数据结构(称为 Expression)的查询的说明。 命令对象具有 ExecuteReader () 方法,该方法导致执行,以 DataReader 的形式返回结果。 IQueryable 对象具有导致执行的 GetEnumerator () 方法,以 IEnumerator<Customer> 的形式返回结果。

因此,如果枚举查询两次,则会执行两次查询。

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

此行为称为 延迟执行。 就像使用 ADO.NET 命令对象一样,可以保留查询并重新执行查询。

当然,应用程序编写器通常需要非常明确执行查询的位置和时间。 如果应用程序只是因为需要多次检查结果而多次执行查询,那将是意外的。 例如,你可能想要将查询的结果绑定到类似 DataGrid 的内容。 控件可以在每次在屏幕上绘制时枚举结果。

为了避免多次执行,请将结果转换为任意数量的标准集合类。 使用标准查询运算符 ToList () 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 

延迟执行的一个好处是查询可以分段构造,执行仅在构造完成时发生。 你可以开始编写查询的一部分,将其分配给局部变量,然后稍后继续向其应用更多运算符。

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 

在此示例中, q 作为伦敦所有客户的查询开始。 之后,它会根据应用程序状态更改为有序查询。 通过延迟执行,可以构造查询以满足应用程序的确切需求,而无需进行有风险的字符串操作。

对象标识

运行时中的对象具有唯一标识。 如果两个变量引用同一个对象,则它们实际上引用同一个对象实例。 因此,通过路径通过一个变量进行的更改通过另一个变量立即可见。 关系数据库表中的行没有唯一标识。 但是,它们确实有一个主键,并且该主键可能是唯一的,这意味着没有两行可以共享同一个密钥。 然而,这只会限制数据库表的内容。 因此,只要我们只通过远程命令与数据交互,它就大致相同。

但是,这种情况很少出现。 大多数情况下,数据会从数据库引入到应用程序对其进行操作的其他层中。 显然,这是 LINQ to SQL 旨在支持的模型。 当数据作为行从数据库中引入时,预计表示相同数据的两行实际上对应于同一行实例。 如果查询特定客户两次,将获得两行数据,每个行包含相同的信息。

然而,对于对象,你期望的东西完全不同。 你预计,如果再次向 DataContext 询问相同的信息,它实际上会返回相同的对象实例。 你期望这一点,因为对象对应用程序具有特殊意义,并且你希望它们的行为与普通对象类似。 你将其设计为层次结构或图形,你当然希望检索它们,而不需要大量复制的实例,只是因为你两次要求相同的操作。

因此, DataContext 管理对象标识。 每当从数据库中检索新行时,都会通过主键将其记录在标识表中,并创建一个新对象。 每当再次检索同一行时,原始对象实例将传回应用程序。 这样, DataContext 会将标识 (密钥) 的数据库概念转换为) 实例 (语言概念。 应用程序只会看到处于首次检索状态的对象。 新数据(如果不同)将丢弃。

你可能会对此感到困惑,因为为什么任何应用程序都会丢弃数据? 事实证明,这是 LINQ to SQL 管理本地对象完整性并能够支持乐观更新的方式。 由于最初创建对象后发生的唯一更改是应用程序所做的更改,因此应用程序的意图是明确的。 如果外部方在在此期间发生了更改,将在调用 SubmitChanges () 时标识这些更改。 有关这一点的详细信息,请参阅“同时更改”部分。

请注意,如果数据库包含没有主键的表,则 LINQ to SQL 允许通过表提交查询,但不允许更新。 这是因为,由于缺少唯一键,框架无法确定要更新的行。

当然,如果查询请求的对象很容易被其主键标识为已检索到的对象,则根本不会执行任何查询。 标识表充当存储以前检索到的所有对象的缓存。

关系

正如我们在快速教程中看到的,类定义中对其他对象或其他对象的集合的引用直接对应于数据库中的外键关系。 查询时,只需使用点表示法访问关系属性,从一个对象导航到另一个对象,即可使用这些关系。 这些访问操作将转换为等效 SQL 中更复杂的联接或关联子查询,使你可以在查询期间遍历对象图。 例如,下面的查询从订单定位到客户,以此方式将结果限制为只包括位于伦敦的客户的订单。

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"

如果关系属性不存在,则必须像在 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

关系属性允许在启用更方便的点语法后定义此特定关系。 但是,这不是关系属性存在的原因。 它们之所以存在,是因为我们倾向于将特定于域的对象模型定义为层次结构或图形。 我们选择针对其进行编程的对象具有对其他对象的引用。 由于对象到对象关系对应于数据库中的外键样式关系,因此属性访问会导致一种方便的联接写入方式,这仅仅是一个令人高兴的巧合。

因此,关系属性的存在在查询的结果端比作为查询本身的一部分更重要。 在拥有特定客户的手后,其类定义会告诉你客户有订单。 因此,当你查看特定客户的 Orders 属性时,你期望看到集合填充了所有客户的订单,因为这实际上是你通过这样定义类声明的协定。 你期望看到那里的订单,即使你没有特别要求预先的订单。 你期望对象模型保持一种假象,即它是数据库的内存中扩展,相关对象立即可用。

LINQ to SQL 实现了一种称为 延迟加载 的技术,以帮助保持这种错觉。 查询对象时,实际上只检索所请求的对象。 不会同时自动获取相关对象。 但是,由于一旦尝试访问相关对象,就会发出检索请求,因此无法观察到尚未加载相关对象这一事实。

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

例如,你可能想要查询一组特定的订单,然后偶尔向特定客户发送电子邮件通知。 无需预先检索每个订单的所有客户数据。 延迟加载允许将检索额外信息的成本推迟到必须进行。

当然,情况可能正好相反。 你可能有一个应用程序需要同时查看客户和订单数据。 您了解同时需要这两组数据。 你知道应用程序会在收到每个客户的订单后立即向下钻取。 很遗憾,为每个客户的订单触发单个查询会很不幸。 真正想要执行的操作是与客户一起检索订单数据。

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

当然,你始终可以通过形成跨产品并检索所有相对数据位作为一个大投影,在查询中将客户和订单联接在一起。 但结果不会是实体。 实体是具有标识的对象,你可以修改这些对象,而结果将是无法更改和持久保存的投影。 更糟的是,当每个客户在平展联接输出中重复每个订单时,你将检索大量冗余数据。

你真正需要的是一种同时检索一组相关对象的方法,即图形的划界部分,因此你永远不会检索任何超出预期用途所需的内容。

LINQ to SQL 允许出于此原因请求 立即加载 对象模型的某个区域。 它通过允许指定 DataContextDataShape 来执行此操作。 DataShape 类用于指示框架在检索特定类型时要检索哪些对象。 这是使用 LoadWith 方法实现的,如下所示:

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

在前面的查询中,在执行查询时,将检索居住在伦敦的所有客户的订单,以便对 Customer 对象上的 Orders 属性的连续访问不会触发数据库查询。

DataShape 类还可用于指定应用于关系导航的子查询。 例如,如果只想检索今天已发货的订单,则可以在 DataShape 上使用 AssociateWith 方法,如下所示:

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

在前面的代码中,内部 foreach 语句仅循环访问今天已发货的 订单 ,因为仅从数据库中检索了此类订单。

请务必注意 有关 DataShape 类的两个事实:

  1. DataShape 分配给 DataContext 后,无法修改 DataShape 。 对此类 DataShape 的任何 LoadWithAssociateWith 方法调用都将在运行时返回错误。

  2. 无法使用 LoadWithAssociateWith 创建周期。 例如,以下在运行时生成错误:

    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)
    

联接

针对对象模型的大多数查询严重依赖于在对象模型中导航对象引用。 但是,实体之间存在一些有趣的“关系”,这些“关系”可能不会在对象模型中作为引用捕获。 例如 ,Customer.Orders 是基于 Northwind 数据库中的外键关系的有用关系。 但是,同一城市或国家/地区的供应商和客户是一种不基于外键关系且不能在对象模型中捕获的 临时 关系。 联接提供了处理此类关系的附加机制。 LINQ to SQL 支持 LINQ 中引入的新联接运算符。

请考虑以下问题 - 查找位于同一城市的供应商和客户。 以下查询以平展结果的形式返回供应商和客户公司名称和公用城市。 这等效于关系数据库中的内部等效联接:

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

上述查询将消除与特定客户不在同一城市中的供应商。 但是,有时我们不希望在 临时 关系中消除其中一个实体。 以下查询列出了具有每个供应商的客户组的所有供应商。 如果特定供应商在同一城市中没有任何客户,则结果是与该供应商对应的空客户集合。 请注意,结果不是平面的,每个供应商都有一个关联的 集合。 实际上,这提供了组联接 - 它将两个序列联接,并将第二个序列的元素与第一个序列的元素进行分组。

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

组联接也可以扩展到多个集合。 以下查询通过列出与供应商位于同一城市的员工来扩展上述查询。 在这里,结果显示 (可能为空) 客户和员工集合的供应商。

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

也可以平展组加入的结果。 平展供应商和客户之间的组加入的结果是,对于所在城市中有多个客户的供应商,每个客户一个。 空集合将替换为 null。 这等效于关系数据库中的左外部等价联接。

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

基础联接运算符的签名在标准查询运算符文档中定义。 仅支持等价联接,并且 相等 的两个操作数必须具有相同的类型。

投影数

到目前为止,我们只查看了用于检索 实体的查询,即与数据库表直接关联的对象。 我们不需要把自己限制在这样。 查询语言的美妙在于,可以检索任何所需格式的信息。 执行此操作时,将无法利用自动更改跟踪或标识管理。 但是,可以仅获取所需的数据。

例如,你可能只需要知道伦敦所有客户的公司名称。 如果是这种情况,则没有特定理由仅为了选取名称而检索整个客户对象。 可以将名称投影为查询的一部分。

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

在这种情况下, q 将成为检索字符串序列的查询。

如果想要返回的不仅仅是单个名称,但不足以证明提取整个客户对象的合理性,可以通过将结果构造为查询的一部分来指定所需的任何子集。

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

此示例使用 匿名对象初始值设定项 创建一个包含公司名称和电话号码的结构。 你可能不知道该如何调用类型,但使用不一定需要的语言隐 式类型局部变量声明

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

如果立即使用数据, 则匿名类型 是显式定义类以保存查询结果的良好替代方法。

也可以形成整个对象的交叉积,但你可能很少有理由这样做。

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

此查询构造客户和订单对象对的序列。

还可以在查询的任何阶段进行投影。 可以将数据投影到新构造的对象中,然后在后续查询操作中引用这些对象的成员。

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

不过,请注意在此阶段使用参数化构造函数。 这样做在技术上是有效的,但在不了解构造函数内部的实际代码的情况下,LINQ to SQL 无法跟踪构造函数使用情况如何影响成员状态。

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

由于 LINQ to SQL 尝试将查询转换为纯关系 SQL,因此本地定义的对象类型在服务器上不可用,无法进行实际构造。 所有对象构造实际上都推迟到从数据库检索回数据之后。 生成的 SQL 使用普通 SQL 列投影代替实际构造函数。 由于查询翻译器无法了解构造函数调用期间发生的情况,因此无法为 MyType“名称”字段建立含义。

相反,最佳做法是始终使用 对象初始值设定项 对投影进行编码。

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

使用参数化构造函数的唯一安全位置是查询的最终投影。

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>

如果需要,甚至可以使用对象构造函数的精心嵌套,例如此示例直接从查询结果构造 XML。 只要它是查询的最后一个投影,它就有效。

不过,即使构造函数调用被理解,也可能不会调用本地方法。 如果最终的投影需要调用本地方法,则 LINQ to SQL 不太可能能够强制执行。 没有已知 SQL 转换的方法调用不能用作查询的一部分。 此规则的一个例外是没有依赖于查询变量的参数的方法调用。 这些不被视为已翻译查询的一部分,而是被视为参数。

仍需详细预测 (转换) 可能需要实现本地过程逻辑。 若要在最终投影中使用自己的本地方法,需要投影两次。 第一个投影提取需要引用的所有数据值,第二个投影执行转换。 在这两个投影之间是对 AsEnumerable () 运算符的调用,该运算符将此时的处理从 LINQ 到 SQL 查询转换为本地执行的查询。

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)}

注意AsEnumerable () 运算符与 ToList () ToArray () 不同,不会导致查询的执行。 它仍然被推迟。 AsEnumerable () 运算符只是更改查询的静态类型,将 Visual Basic) 中的 IQueryable<T> (IQueryable) (转换为 Visual Basic) 中 T) 的 IEnumerable<> (IEnumerable (,诱使编译器将其余查询视为本地执行。

编译的查询

在许多应用程序中,经常多次执行结构相似的查询。 在这种情况下,可以通过编译一次查询并在应用程序中使用不同的参数多次执行查询来提高性能。 此结果是使用 CompiledQuery 类在 LINQ to SQL 中获取的。 以下代码演示如何定义已编译的查询:

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

Compile 方法返回一个委托,该委托以后只需更改输入参数即可缓存和执行多次。 下面的代码显示了此用法的示例:

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

SQL 翻译

LINQ to SQL 实际上不执行查询;关系数据库会这样做。 LINQ to SQL 将你编写的查询转换为等效的 SQL 查询,并将其发送到服务器进行处理。 由于执行是延迟的,因此 LINQ to SQL 能够检查整个查询,即使从多个部分组合。

由于关系数据库服务器实际上不执行 IL (除了 SQL Server 2005) 中的 CLR 集成之外,查询不会作为 IL 传输到服务器。 实际上,它们以文本形式作为参数化 SQL 查询传输。

当然,SQL(甚至 T-SQL 与 CLR 集成)无法执行程序本地可用的各种方法。 因此,编写的查询必须转换为 SQL 环境中可用的等效操作和函数。

.Net Framework 内置类型上的大多数方法和运算符都直接转换为 SQL。 有些可以从可用的函数中生成。 不允许转换的异常,如果尝试使用这些异常,则会生成运行时异常。 本文档后面的一节详细介绍了要转换为 SQL 的框架方法。

实体生命周期

LINQ to SQL 不仅仅是关系数据库的标准查询运算符的实现。 除了转换查询外,它还是一项服务,用于在对象的整个生存期内对其进行管理,有助于维护数据的完整性,并自动执行将修改转换回存储的过程。

在典型方案中,通过一个或多个查询检索对象,然后以某种或那样的方式操作对象,直到应用程序准备好将更改发送回服务器。 此过程可能会重复多次,直到应用程序不再使用此信息。 此时,运行时将回收对象,就像普通对象一样。 但是,数据仍保留在数据库中。 即使从运行时存在中擦除,仍可检索表示相同数据的对象。 从这个意义上说,对象的真实生存期存在于任何单一运行时表现形式之外。

本章的重点是 实体生命周期 ,其中周期是指实体对象在特定运行时上下文中的单个表现形式的时间跨度。 当 DataContext 感知到新实例时,循环开始,在不再需要对象或 DataContext 时结束。

跟踪更改

从数据库检索实体后,可以随意操作它们。 它们是你的对象;请根据需要使用它们。 执行此操作时,LINQ to SQL 会跟踪更改,以便在调用 SubmitChanges () 时将其保存到数据库中。

LINQ to SQL 在从数据库中检索实体的那一刻起开始跟踪实体,然后你再用手操作它们。 事实上,前面讨论的 标识管理服务 也已启动。 在实际开始进行更改之前,更改跟踪在额外的开销中花费很少。

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"

一旦在上面的示例中分配了 CompanyName ,LINQ to SQL 就会意识到更改并能够记录更改。 所有数据成员的原始值由 更改跟踪服务保留。

更改跟踪服务还记录关系属性的所有操作。 使用关系属性在实体之间建立链接,即使这些链接可能由数据库中的键值链接。 无需直接修改与键列关联的成员。 在提交更改之前,LINQ to SQL 会自动为你同步它们。

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

只需对其 Customer 属性进行分配,即可将订单从一个客户移动到另一个 客户 。 由于客户和订单之间存在关系,因此可以通过修改任一方来更改关系。 可以同样轻松地从cust2 Orders 集合中删除它们,并将其添加到 cust1 的 orders 集合,如下所示。

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)

当然,如果为关系分配 null 值,则实际上可以完全消除关系。 将订单的 Customer 属性分配到 null 实际上会从客户的列表中删除订单。

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))

关系双方的自动更新对于保持对象图的一致性至关重要。 与普通对象不同,数据之间的关系通常是双向的。 LINQ to SQL 允许使用属性来表示关系。 但是,它不提供自动使这些双向属性保持同步的服务。这是一个服务级别,必须直接嵌入到类定义中。 使用代码生成工具生成的实体类具有此功能。 在下一章中,我们将向你展示如何对你自己的手写类执行此操作。

但是,请务必注意,删除关系并不意味着对象已从数据库中删除。 请记住,基础数据的生存期一直保留在数据库中,直到从表中删除该行。 实际删除对象的唯一方法是将其从其 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)

与所有其他更改一样,订单实际上尚未删除。 它看起来就像我们一样,因为它已被删除并分离我们的其余对象。 从 Orders 表中删除 order 对象时,更改跟踪服务已将其标记为要删除。 在调用 SubmitChanges () 时提交更改时,将从数据库实际删除。 请注意,永远不会删除对象本身。 运行时管理对象实例的生存期,因此只要你仍然持有对它的引用,它就一直存在。 但是,从其 中删除对象并提交更改后,更改跟踪服务将不再跟踪该对象。

唯一未跟踪实体的其他时间是,在 DataContext 意识到实体之前该实体存在。 每当在代码中创建新对象时,都会发生这种情况。 可以在应用程序中自由使用实体类的实例,而无需从数据库中检索它们。 更改劫持和标识管理仅适用于 DataContext 知道的那些对象。 因此,在将新创建的实例添加到 DataContext 之前,不会为它们启用这两项服务。

这可以通过两种方式之一发生。 可以手动对相关的 Table 集合调用 Add () 方法。

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)

或者,可以将新实例附加到 DataContext 已识别的对象。

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 } )

即使新对象实例附加到其他新实例, DataContext 也会发现这些对象实例。

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 
      }
   } )

基本上,无论是否调用 Add () 方法,DataContext 都会识别对象图中当前未作为新实例跟踪的任何实体。

使用只读 DataContext

许多方案不需要更新从数据库检索到的实体。 在网页上显示“客户”表是一个明显示例。 在所有此类情况下,都可以通过指示 DataContext 不跟踪对实体的更改来提高性能。 这是通过将 DataContext 上的 ObjectTracking 属性指定为 false 来实现的,如以下代码所示:

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

提交更改

无论对对象进行了多少更改,这些更改都只对内存中副本进行了更改。 数据库中的实际数据尚未发生任何变化。 在通过调用 DataContext 上的 SubmitChanges () 显式请求之前,不会将此信息传输到服务器。

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()

调用 SubmitChanges () 时, DataContext 将尝试将所有更改转换为等效的 SQL 命令,插入、更新或删除相应表中的行。 如果需要,这些操作可由你自己的自定义逻辑替代,但提交顺序由 DataContext 的服务(称为 更改处理器)来协调。

调用 SubmitChanges () 时发生的第一件事是检查已知对象集以确定是否已附加新实例。 这些新实例将添加到跟踪对象集中。 接下来,所有具有挂起更改的对象将基于它们之间的依赖关系排序为一系列对象。 更改依赖于其他对象的那些对象在其依赖项之后进行排序。 数据库中的外键约束和唯一性约束在确定更改的正确顺序方面起着重要作用。 然后,在传输任何实际更改之前,将启动事务来封装单个命令系列,除非其中一个命令已在范围内。 最后,对对象的更改逐个转换为 SQL 命令并发送到服务器。

此时,数据库检测到的任何错误都将导致提交过程中止,并引发异常。 将回滚对数据库所做的所有更改,就好像从未进行过任何提交一样。 DataContext 仍将完整记录所有更改,因此可以尝试纠正问题并通过再次调用 SubmitChanges () 重新提交这些问题。

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

提交事务成功完成后, DataContext 将接受对对象的更改,只需忘记更改跟踪信息即可。

同时更改

SubmitChanges () 的调用可能会失败有多种原因。 你可能创建了一个具有无效主键的对象;一个已在使用中,或者具有违反数据库的某些检查约束的值。 此类检查很难融入业务逻辑,因为它们通常需要对整个数据库状态的绝对了解。 但是,最可能失败的原因只是其他人对你之前对对象进行了更改。

当然,如果锁定数据库中的每个对象并使用完全序列化的事务,则这是不可能的。 但是,这种编程风格 (悲观并发) 很少使用,因为它很昂贵,而且很少发生真正的冲突。 管理同时更改的最常用形式是采用 乐观并发形式。 在此模型中,根本不会针对数据库行执行锁。 这意味着,在首次检索对象和提交更改之间,数据库可能发生任意数量的更改。

因此,除非你想要使用上次更新获胜的策略,擦除之前发生的任何其他内容,否则你可能希望收到有关其他人更改基础数据这一事实的警报。

DataContext 通过自动检测更改冲突,内置了对乐观并发的支持。 仅当数据库的当前状态与首次检索对象时所了解的数据的状态匹配时,单个更新才会成功。 这种情况基于每个对象发生,仅当冲突发生在已更改的对象上时,才会向你发出警报。

定义实体类时,可以控制 DataContext 检测更改冲突的程度。 每个 Column 属性都有一个名为 UpdateCheck 的属性,该属性可分配以下三个值之一: AlwaysNeverWhenChanged。 如果未设置 Column 属性的默认值为 Always,这意味着始终检查该成员表示的数据值是否存在冲突,也就是说,除非存在明显的关联破坏程序(如版本标记)。 Column 属性具有 IsVersion 属性,可用于指定数据值是否构成由数据库维护的版本标记。 如果存在版本,则单独使用该版本来确定是否发生了冲突。

发生更改冲突时,将引发异常,就像它是任何其他错误一样。 围绕提交的事务将中止,但 DataContext 将保持不变,使你有机会纠正问题并重试。

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

如果要在中间层或服务器上进行更改,若要纠正更改冲突,最简单的方法是只需重新开始并重试,重新创建上下文并重新应用更改。 下一节将介绍其他选项。

事务

事务是由数据库或任何其他资源管理器提供的服务,可用于保证自动发生一系列单独的操作;这意味着他们要么都成功,要么都没有。 如果没有,则它们也会在允许发生任何其他操作之前自动撤消。 如果没有事务已在范围内,则当您调用 SubmitChanges () 时,DataContext 将自动启动数据库事务以保护更新。

可以选择通过自行启动它来控制使用的事务类型、其隔离级别或它实际包含的内容。 DataContext 将使用的事务隔离称为 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

上面的示例通过创建新的事务范围对象来启动完全序列化的事务。 在事务范围内执行的所有数据库命令都将受到事务保护。

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

同一示例的此修改版本在提交更改之前,使用 DataContext 上的 ExecuteCommand () 方法执行数据库中的存储过程。 无论存储过程对数据库执行什么操作,我们都可以确定其操作是同一事务的一部分。

如果事务成功完成, DataContext 将抛出所有累积的跟踪信息,并将实体的新状态视为未更改。 但是,如果事务失败,它不会回滚对对象的更改。 这样,便可以在提交更改期间处理问题时获得最大的灵活性。

也可以使用本地 SQL 事务而不是新的 TransactionScope。 LINQ to SQL 提供此功能,可帮助你将 LINQ to SQL 功能集成到预先存在的 ADO.NET 应用程序中。 但是,如果你走这条路,你需要承担更多责任。

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

如你所看到的,使用手动控制的数据库事务有点复杂。 你不仅必须自行启动它,而且必须通过将 DataContext 分配给 Transaction 属性来显式告知它使用。 然后,必须使用 try-catch 块来包裹提交逻辑,记住显式告知事务要提交,并显式告知 DataContext 接受更改,或者在任何时间点发生故障时中止事务。 此外,在完成后,不要忘记将 Transaction 属性重新设置为 null

存储过程

调用 SubmitChanges () 时,LINQ to SQL 生成并执行 SQL 命令以插入、更新和删除数据库中的行。 应用程序开发人员可以重写这些操作,并在其位置使用自定义代码来执行所需的操作。 这样, 更改处理器可以自动调用数据库存储过程等替代设施。

请考虑一个存储过程,用于更新 Northwind 示例数据库中 Products 表的库存单位。 该过程的 SQL 声明如下所示。

SQL

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

可以通过在强类型 DataContext 上定义方法,使用存储过程而不是常规自动生成的更新命令。 即使 DataContext 类是由 LINQ to SQL 代码生成工具自动生成的,你仍然可以在自己的分部类中指定这些方法。

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

方法和泛型参数的签名告知 DataContext 使用此方法代替生成的 update 语句。 原始参数和当前参数由 LINQ to SQL 用于传入指定类型的对象的原始副本和当前副本。 这两个参数可用于乐观并发冲突检测。

注意 如果替代默认更新逻辑,则冲突检测由你负责。

存储过程 UpdateProductStock 是使用 DataContextExecuteCommand () 方法调用的。 它返回受影响的行数,并具有以下签名:

C#

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

Visual Basic

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

对象数组用于传递执行 命令所需的参数。

与 update 方法类似,可以指定 insert 和 delete 方法。 插入和删除方法只需更新实体类型的一个参数。 例如,可以按如下所示指定插入和删除 Product 实例的方法:

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)  ... 

实体类In-Depth

使用属性

实体类与你可能定义为应用程序的一部分的任何普通对象类一样,只不过,它使用特殊信息进行批注,以便将其与特定的数据库表相关联。 这些批注作为类声明中的自定义属性。 仅当将 类与 LINQ to SQL 结合使用时,这些属性才有意义。 它们类似于.NET Framework中的 XML 序列化属性。 这些“数据”属性为 LINQ to SQL 提供了足够的信息,以便将针对对象的查询转换为针对数据库的 SQL 查询,并将对象更改转换为 SQL 插入、更新和删除命令。

还可以通过使用 XML 映射文件而不是属性来表示映射信息。 外部映射部分更详细地描述了此方案。

Database 属性

如果连接未提供数据库的默认名称,则 Database 属性用于 指定数据库的默认名称。 数据库 属性可以应用于强类型的 DataContext 声明。 此属性是可选的。

Database 属性

属性 类型 说明
名称 String 指定数据库的名称。 仅当连接本身未指定数据库名称时,才使用该信息。 如果上下文声明中不存在此 Database 属性,并且连接未指定一个属性,则假定数据库与上下文类同名。

C#

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

Visual Basic

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

表属性

Table 属性用于将类指定为与数据库表关联的实体类。 具有 Table 属性的类将由 LINQ to SQL 专门处理。

表属性

属性 类型 说明
名称 String 指定表的名称。 如果未指定此信息,则假定表与实体类同名。

C#

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

Visual Basic

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

列属性

Column 属性用于指定表示数据库表中列的实体类的成员。 它可应用于任何字段或属性(公共、专用或内部)。 当 LINQ to SQL 将更改保存到数据库时,仅保留标识为列的成员。

列属性

属性 类型 说明
名称 String 表或视图中列的名称。 如果未指定,则假定该列与类成员同名。
存储 字符串 基础存储的名称。 如果指定,它将告知 LINQ to SQL 如何绕过数据成员的公共属性访问器,并与原始值本身交互。 如果未指定,则 LINQ to SQL 使用公共访问器获取和设置值。
DBType 字符串 使用数据库类型和修饰符指定的数据库列的类型。 这是用于在 T-SQL 表声明命令中定义列的确切文本。 如果未指定,则从成员类型推断数据库列类型。 仅当应使用 CreateDatabase () 方法创建数据库实例时,才需要特定的数据库类型。
IsPrimaryKey Bool 如果设置为 true,则类成员表示作为表主键一部分的列。 如果类的多个成员被指定为 ID,则主键称为关联列的复合。
IsDbGenerated 布尔 标识成员的列值是由数据库自动生成的。 指定为 IsDbGenerated=true 的主键还应具有带 IDENTITY 修饰符的 DBTypeIsDbGenerated 成员在插入数据行后立即同步,并在 SubmitChanges () 完成后可用。
IsVersion 布尔 将成员的列类型标识为数据库时间戳或版本号。 每次更新关联的行时,数据库都会递增版本号并更新时间戳列。 IsVersion=true 的成员在更新数据行后立即同步。 新值在 SubmitChanges () 完成后可见。
UpdateCheck UpdateCheck 确定 LINQ to SQL 如何实现 乐观并发 冲突检测。 如果没有将成员指定为 IsVersion=true ,则通过比较原始成员值与当前数据库状态来完成检测。 可以通过为每个成员提供 UpdateCheck 枚举值来控制 LINQ to SQL 在冲突检测期间使用哪些成员。
  • 始终:始终使用此列进行冲突检测
  • 从不:从不将此列用于冲突检测
  • WhenChanged:仅当应用程序更改了成员时才使用此列
IsDiscriminator 布尔 确定类成员是否持有继承层次结构的鉴别器值。
表达式 String 不影响 LINQ to SQL 的操作,但在 期间使用。CreateDatabase () 作为表示计算列表达式的原始 SQL 表达式。
CanBeNull 布尔 指示该值可以包含 null 值。 这通常从实体成员的 CLR 类型推断而来。 使用此属性可指示字符串值表示为数据库中不可为 null 的列。
AutoSync AutoSync 指定是否在插入或更新命令时从数据库生成的值自动同步列。 此标记的有效值为 OnInsertAlwaysNever

典型的实体类将在公共属性上使用 Column 属性,并将实际值存储在私有字段中。

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

仅指定 DBType ,以便 CreateDatabase () 方法可以使用最精确的类型构造表。 否则,将不使用基础列限制为 15 个字符的知识。

表示数据库类型的主键的成员通常与自动生成的值相关联。

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

如果指定 DBType,请确保包含 IDENTITY 修饰符。 LINQ to SQL 不会扩充自定义指定的 DBType。 但是,如果 DBType 未指定,LINQ to SQL 将推断在通过 CreateDatabase () 方法创建数据库时需要 IDENTITY 修饰符。

同样,如果 IsVersion 属性为 true,则 DBType 必须指定正确的修饰符来指定版本号或时间戳列。 如果未指定 DBType ,LINQ to SQL 将推断正确的修饰符。

可以通过指定成员的访问级别,甚至限制访问器本身来控制对与自动生成的列、版本标记或你可能想要隐藏的任何列关联的成员的访问。

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

订单的 CustomerID 属性可以通过不定义 set 访问器而设为只读。 LINQ to SQL 仍可以通过存储成员获取和设置基础值。

还可以通过在私有成员上放置 Column 属性,使成员完全无法访问应用程序的其余部分。 这允许实体类包含与类的业务逻辑相关的信息,而无需公开一般信息。 尽管私有成员是已翻译数据的一部分,但由于它们是私有成员,因此无法在语言集成查询中引用它们。

默认情况下,所有成员都用于执行乐观并发冲突检测。 可以通过指定特定成员的 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

下表显示了数据库类型和相应 CLR 类型之间的允许映射。 确定要用于表示特定数据库列的 CLR 类型时,请使用此表作为指南。

数据库类型和相应的 CLR 类型允许映射

数据库类型 .NET CLR 类型 注释
bit、tinyint、smallint、int、bigint Bye、Int16、Uint16、Int32、Uint32、Int64、Uint64 可能进行有损转换。 值不能往返。
bit 布尔  
decimal、numeric、smallmoney、money 小数 缩放差异可能会导致有损转换。 不能往返。
real、float Single、Double 精度差异。
char、varchar、text、nchar、nvarchar、ntext 字符串 可能存在区域设置差异。
datetime、smalldatetime DateTime 不同的精度可能会导致有损转换和往返问题。
uniqueidentifier Guid 不同的排序规则。 排序可能无法按预期工作。
timestamp Visual Basic) 、Binary 中的 Byte[] (Byte () 字节数组被视为标量类型。 调用构造函数时,用户负责分配足够的存储。 它被视为不可变,不会跟踪更改。
binary、varbinary Visual Basic) 、Binary 中的 Byte[] (Byte ()  

关联属性

Association 属性用于指定表示数据库关联的属性,例如外键到主键的关系。

关联属性

属性 类型 说明
名称 String 关联的名称。 这通常与数据库的外键约束名称相同。 当 CreateDatabase () 用于创建数据库实例以生成相关约束时,将使用它。 它还用于帮助区分引用同一目标实体类的单个实体类中的多个关系。 在这种情况下,如果两者都定义了关系,则关系两侧的关系属性 () 必须具有相同的名称。
存储 字符串 基础存储成员的名称。 如果指定,它将告知 LINQ to SQL 如何绕过数据成员的公共属性访问器,并与原始值本身交互。 如果未指定,则 LINQ to SQL 使用公共访问器获取和设置值。 建议所有关联成员都是标识了单独存储成员的属性。
ThisKey 字符串 此实体类的一个或多个成员的名称以逗号分隔的列表,这些成员表示关联的此端的键值。 如果未指定,则假定成员是构成主键的成员。
OtherKey 字符串 目标实体类的一个或多个成员的名称的逗号分隔列表,这些成员表示关联另一端的键值。 如果未指定,则假定成员是构成其他实体类的主键的成员。
IsUnique 布尔 如此 如果 外键的唯一性约束,指示真正的 1:1 关系。 此属性很少使用,因为几乎无法在数据库中管理 1:1 关系。 大多数实体模型是使用 1:n 关系定义的,即使应用程序开发人员将其视为 1:1 也是如此。
IsForeignKey 布尔 如此 如果 关联的目标“其他”类型是源类型的父级。 使用外键到主键关系时,持有外键的一方是子级,持有主键的一方是父级。
DeleteRule 字符串 用于向此关联添加删除行为。 例如,“CASCADE”会将“ON DELETE CASCADE”添加到 FK 关系。 如果设置为 null,则不添加删除行为。

关联属性表示对另一个实体类实例的单个引用,或者表示引用的集合。 必须在实体类中使用 EntityRef<T> (EntityRef (Visual Basic 中的 OfT) ) 值类型对单一实例引用进行编码,以存储实际引用。 EntityRef 类型是 LINQ to SQL 启用延迟加载引用的方式。

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

公共属性的类型为 Customer,而不是 EntityRef<Customer>。 请务必不要将 EntityRef 类型公开为公共 API 的一部分,因为查询中对此类型的引用不会转换为 SQL。

同样,表示集合的关联属性必须使用 Visual Basic) 集合类型中的 EntitySet<T> (EntitySet (OfT ) 来存储关系。

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

但是,由于 Visual Basic) 中的 EntitySet<T> (EntitySet (OfT ) 是集合,因此使用 EntitySet 作为返回类型是有效的。 改用 Visual Basic) 接口中的 ICollection<T> (ICollection (OfT) 来伪装集合的真实类型也有效。

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

如果公开属性的公共资源库,请确保在 EntitySet 上使用 Assign () 方法。 这允许实体类继续使用同一集合实例,因为它可能已绑定到 更改跟踪服务

ResultType 属性

此属性指定可从声明为返回 IMultipleResults 接口的函数返回的可枚举序列的元素类型。 可以多次指定此属性。

ResultType 属性

属性 类型 说明
类型 类型 返回结果的类型。

StoredProcedure 属性

StoredProcedure 属性用于声明对 DataContextSchema 类型上定义的方法的调用被转换为对数据库存储过程的调用。

StoredProcedure 属性

属性 类型 说明
名称 String 数据库中存储过程的名称。 如果未指定,则假定存储过程与 方法同名

函数属性

Function 属性用于声明对 DataContextSchema 上定义的方法的调用将转换为对数据库用户定义的标量函数或表值函数的调用。

函数属性

属性 类型 说明
名称 String 数据库中函数的名称。 如果未指定,则假定函数的名称与 方法相同

参数属性

Parameter 属性用于声明方法与数据库存储过程或用户定义函数的参数之间的映射。

参数属性

属性 类型 说明
名称 String 数据库中参数的名称。 如果未指定,则从方法参数名称推断参数。
DBType 字符串 使用数据库类型和修饰符指定的参数的类型。

InheritanceMapping 属性

InheritanceMapping 属性用于描述特定鉴别器代码与继承子类型之间的对应关系。 用于继承层次结构的所有 InheritanceMapping 属性都必须在层次结构的根类型上声明。

InheritanceMapping 属性

属性 类型 说明
代码 Object 鉴别器代码值。
类型 类型 继承子类型。 这可能是继承层次结构中的任何非抽象类型,包括根类型。
IsDefault 布尔 确定指定的继承子类型是否为当 LINQ to SQL 找到未由 InheritanceMapping 属性定义的鉴别器代码时构造的默认类型。 只有一个 InheritanceMapping 属性必须使用 IsDefault 声明为 true。

图形一致性

图形是对象数据结构的一个通用术语,这些对象都通过引用相互引用。 层次结构 (或树) 是一种退化的图形形式。 特定于域的对象模型通常描述引用网络,这些引用最好地描述为对象的图。 对象图的运行状况对应用程序的稳定性至关重要。 因此,请务必确保关系图中的引用与数据库中定义的业务规则和/或约束保持一致。

LINQ to SQL 不会自动为你管理关系引用的一致性。 当关系是双向关系时,对关系一方的更改应自动更新另一方。 请注意,正常对象的行为方式并不常见,因此你不太可能以这种方式设计对象。

LINQ to SQL 确实提供了一些机制来简化这项工作,并提供了一种模式供你遵循,以确保正确管理引用。 代码生成工具生成的实体类将自动实现正确的模式。

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
);}

Visual Basic) 类型中的 EntitySet<T> (EntitySet (OfT) 具有一个构造函数,该构造函数允许提供两个用作回调的委托;第一个委托在将项添加到集合时,第二个当项被删除时。 如示例中所示,可以并且应该编写为这些委托指定的代码来更新反向关系属性。 这是将订单添加到客户的 Orders 集合时,订单实例上的 Customer 属性自动更改的方式。

另一端实现关系并不容易。 Visual Basic) 中的 EntityRef<T> (EntityRef (OfT ) 是一种值类型,定义为尽可能少地包含实际对象引用的额外开销。 它没有空间容纳一对代表。 相反,管理单一实例引用的图形一致性的代码应嵌入到属性访问器本身中。

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

看看资源库。 更改 Customer 属性时,订单实例首先从当前客户的 Orders 集合中删除,然后才添加到新客户的集合中。 请注意,在调用 Remove () 之前,实际实体引用设置为 null。 这样做是为了避免在调用 Remove () 方法时出现递归。 请记住, EntitySet 将使用回调委托将此对象的 Customer 属性分配给 null。 同样的事情发生在调用 Add () 之前。 实际实体引用将更新为新值。 这将再次减少任何潜在的递归,当然,首先完成资源库的任务。

一对一关系的定义与单一实例引用一侧的一对多关系定义非常相似。 分配新的对象或分配 null 来断断关系,而不是调用 Add () Remove ()

同样,关系属性保持对象图的一致性至关重要。 如果内存中对象图与数据库数据不一致,则在调用 SubmitChanges 方法时将生成运行时异常。 请考虑使用代码生成工具来维护一致性工作。

更改通知

对象可以参与更改跟踪过程。 不需要这样做,但它们可以大幅减少跟踪潜在对象更改所需的开销量。 应用程序从查询中检索的对象可能比最终被修改的对象多得多。 如果没有对象的主动帮助,更改跟踪服务的实际跟踪更改将受到限制。

由于运行时中没有真正的拦截服务,因此实际上不会进行正式跟踪。 相反,在首次检索对象时,会存储对象的重复副本。 稍后调用 SubmitChanges () 时,这些副本将用于与已给定的副本进行比较。 如果它们的值不同,则表示对象已被修改。 这意味着,每个对象都需要内存中的两个副本,即使你从不更改它们。

更好的解决方案是让对象本身在确实发生更改时向更改跟踪服务报出。 这可以通过让 对象实现公开回调事件的接口来实现。 然后,更改跟踪服务可以连接每个对象,并在更改时接收通知。

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

为了帮助改进更改跟踪,实体类必须实现 INotifyPropertyChanging 接口。 它只需要定义一个名为 PropertyChanging 的事件-更改跟踪服务随后在对象进入其所有权时向事件注册。 只需在即将更改属性值之前立即引发此事件即可。

不要忘记在关系属性资源库中放置相同的引发逻辑的事件。 对于 EntitySets,请在提供的委托中引发事件。

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

继承

LINQ to SQL 支持单表映射,从而将整个继承层次结构存储在单个数据库表中。 该表包含整个层次结构中所有可能的数据列的平展联合,并且每一行在不适用于行所表示实例类型的列中具有 null。 单表映射策略是最简单的继承表示形式,为许多不同类别的查询提供了良好的性能特征。

映射

若要使用 LINQ to SQL 实现此映射,需要在继承层次结构的根类上指定以下属性和属性:

  • [Table] (<Visual Basic 中的 Table>) 属性。
  • [InheritanceMapping] (<Visual Basic 中的继承映射>) 层次结构中的每个类的属性。 对于非抽象类,此属性必须定义 Code 属性 (“继承鉴别器”列的数据库表中显示的值,以指示此数据行属于哪个类或子类) 和 Type 属性 (该属性指定键值表示) 的类或子类。
  • Visual Basic 中的单个 [InheritanceMapping] 上的 IsDefault 属性 (<InheritanceMapping>) 属性。 此属性用于指定“回退”映射,以防数据库表中的鉴别器值与继承映射中的任何 Code 值都不匹配。
  • Visual Basic 中 [Column] (<Column>) 属性的 IsDiscriminator 属性,表示这是保存继承映射的 Code 值的列。

子类中不需要具有特殊属性 (Attribute) 或属性 (Property)。 特别要注意的是,子类没有 Visual Basic 中的 [Table] (<Table>) 属性。

在以下示例中, CarTruck 子类中包含的数据映射到单一数据库表 Vehicle。 (为了简化示例,示例代码使用字段而不是列映射的属性。)

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

类图如下所示:

图 1. 车辆类图

在服务器资源管理器中查看生成的数据库关系图时,会看到列已全部映射到单个表,如下所示:

图 2. 映射到单个表的列

请注意,表示子类型中字段的列的类型必须为 null,或者需要指定默认值。 这是插入命令成功所必需的。

查询

以下代码提供了如何在查询中使用派生类型的风格:

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

高级

可以将层次结构扩展到已提供的简单示例之外。

示例 1

下面是更深层次、更复杂的查询:

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)

示例 2

以下层次结构包括接口:

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

可能的查询包括:

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))

高级主题

创建数据库

由于实体类具有描述关系数据库表和列结构的属性,因此可以使用此信息创建新的数据库实例。 可以调用 DataContext 上的 CreateDatabase () 方法,让 LINQ to SQL 使用对象定义的结构构造新的数据库实例。 可能需要执行此操作的原因有很多:你可能正在构建一个可在客户系统上自动安装自己的应用程序,或者生成需要本地数据库来保存其脱机状态的客户端应用程序。 对于这些方案,CreateDatabase () 是理想的选择,尤其是在已知数据提供程序(如 SQL Server Express 2005)可用时。

但是,数据属性可能不会对现有数据库结构的所有内容进行编码。 用户定义的函数、存储过程、触发器和检查约束的内容不由 属性表示。 CreateDatabase () 函数将仅使用它所知道的信息(即数据库的结构和每个表中的列类型)创建数据库的副本 (replica) 。 但是,对于各种数据库,这已足够。

下面是如何创建名为 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

对象模型可用于使用 SQL Server Express 2005 创建数据库,如下所示:

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 还提供了一个 API,用于在创建新数据库之前删除现有数据库。 可以将上述数据库创建代码修改为首先使用 DatabaseExists () 为数据库的现有版本检查,然后使用 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()

调用 CreateDatabase () 后,新数据库能够接受 SubmitChanges () 等查询和命令,以将对象添加到 MDF 文件。

还可以使用 MDF 文件或目录名称将 CreateDatabase () 与 SQL Server Express 以外的 SKU 一起使用。 这完全取决于用于连接字符串的内容。 连接字符串中的信息用于定义将存在的数据库,不一定是已经存在的数据库。 LINQ to SQL 将收集相关的信息位,并使用它来确定要创建哪个数据库,以及创建它在哪个服务器上。 当然,你需要在服务器上拥有数据库管理员权限或等效权限才能执行此操作。

与 ADO.NET 互操作

LINQ to SQL 是 ADO.NET 系列技术的一部分, 它基于 ADO.NET 提供程序模型提供的服务,因此可以将 LINQ to SQL 代码与现有 ADO.NET 应用程序混合使用。

创建 LINQ to SQL DataContext 时,可以为它提供现有的 ADO.NET 连接。 针对 DataContext 的所有操作(包括查询)都将使用你提供的连接。 如果连接已打开,则 LINQ to SQL 将尊重你对连接的权限,并在连接完成后保持原样。 通常,除非事务在范围内,否则 LINQ to SQL 在操作完成后立即关闭其连接。

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

始终可以通过 Connection 属性访问 DataContext 所使用的连接,并自行关闭它。

C#

db.Connection.Close(); 

Visual Basic

db.Connection.Close()

还可以为 DataContext 提供你自己的数据库事务,以防应用程序已启动一个数据库事务,并且你希望 DataContext 与它一起运行。

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

每当设置 事务 时, DataContext 都会在发出查询或执行命令时使用它。 完成后,不要忘记将属性分配回 null

但是,使用 .NET Framework 执行事务的首选方法是使用 TransactionScope 对象。 它允许跨数据库和其他内存驻留资源管理器进行分布式事务。 其理念是,事务范围开始时成本很低,仅在事务范围内实际引用多个数据库或多个连接时,才在分布式事务上将自身提升为完全。

C#

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

Visual Basic

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

直接执行 SQL 语句

连接和事务不是可与 ADO.NET 互操作的唯一方法。 在某些情况下,你可能会发现 ,DataContext 的查询或提交更改功能不足以完成你可能想要执行的专用任务。 在这些情况下,可以使用 DataContext 直接向数据库发出原始 SQL 命令。

ExecuteQuery () 方法允许执行原始 SQL 查询,并将查询结果直接转换为对象。 例如,假设 Customer 类的数据分散在 customer1customer2 的两个表上,则以下查询将返回 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" )

只要表格结果中的列名与实体类 LINQ to SQL 的列属性匹配,就会从任何 SQL 查询中具体化对象。

ExecuteQuery () 方法也允许参数。 在以下代码中,将执行参数化查询:

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" )

参数使用 Console.WriteLine () 和 String.Format () 使用的相同 curly 表示法在查询文本中表示。 事实上, String.Format () 实际上是在提供的查询字符串上调用的,将大括号参数替换为生成的参数名称,如 p0@p1 ...p (n)

更改冲突解决

说明

当客户端尝试将更改提交到 对象,并且自客户端上次读取更新检查在数据库中更新了一个或多个值时,会发生更改冲突

注意 只有映射为 UpdateCheck.AlwaysUpdateCheck.WhenChanged 的成员才会参与乐观并发检查。 不会对标记为 UpdateCheck.Never 的成员执行任何检查。

此冲突的解决包括发现对象的哪些成员存在冲突,然后决定如何处理该冲突。 请注意,在特定情况下,乐观并发可能不是最佳策略。 有时,“让上次更新获胜”是完全合理的。

检测、报告和解决 LINQ to SQL 中的冲突

冲突解决是通过再次查询数据库并协调任何差异来刷新冲突项的过程。 刷新对象时,更改跟踪器具有旧的原始值和新数据库值。 然后,LINQ to SQL 确定对象是否冲突。 如果是,则 LINQ to SQL 确定涉及哪些成员。 如果成员的新数据库值不同于用于更新) 失败检查的旧原始 (,则这是一个冲突。 任何成员冲突都添加到冲突列表中。

例如,在以下方案中,User1 开始通过查询数据库中的行来准备更新。 在 User1 提交更改之前,User2 已更改数据库。 User1 的提交失败,因为 Col B 和 Col C 的预期值已更改。

数据库更新冲突

用户 Col A Col B Col C
原始状态 Alfreds Maria Sales
用户 1 Alfred   Marketing
用户 2   Mary 服务

在 LINQ to SQL 中,由于乐观并发冲突而无法更新的对象会导致引发 (ChangeConflictException) 异常。 可以指定是否应在第一次失败时引发异常,或者是否应尝试所有更新,同时在异常中累积并报告任何失败。

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

引发时,异常提供对 ObjectChangeConflict 集合的访问权限。 对于映射到单个失败更新尝试) 的每个冲突 (,包括对 MemberConflicts 列表的访问,详细信息可用。 每个成员冲突映射到未通过并发检查的更新中的单个成员。

冲突处理

在前面的方案中,User1 具有下述 RefreshMode 选项,用于在尝试重新提交之前协调差异。 在所有情况下,首先通过从数据库拉取更新的数据来“刷新”客户端上的记录。 此操作可确保下一次更新尝试不会在同一并发检查中失败。

在此处,User1 选择将数据库值与当前客户端值合并,以便仅当当前更改集也修改了该值时,才会覆盖数据库值。 (请参阅本节后面的示例 1。)

在上述方案中,冲突解决后,数据库中的结果如下所示:

KeepChanges

  Col A Col B Col C
KeepChanges Alfred (User 1) Mary (User 2) 营销 (用户 1)
  • 科尔 A:用户 1 的更改 (显示阿尔弗雷德) 。
  • Col B:用户 2 的更改 (Mary) 出现。 此值已合并,因为 User1 尚未更改它。
  • Col C:用户 1 的更改 (营销) 显示。 User2 的更改 (服务) 未合并,因为 User1 也更改了该项。

在下面,User1 选择使用当前值覆盖任何数据库值。 (请参阅本节后面的示例 2。)

刷新后,将提交 User1 的更改。 数据库中的结果如下所示:

KeepCurrentValues

  Col A Col B Col C
KeepCurrentValues Alfred (User 1) 玛丽亚 (原创) 营销 (用户 1)
  • 科尔 A:用户 1 的更改 (显示阿尔弗雷德) 。
  • B:原来的玛丽亚留下:放弃 User2 的更改。
  • Col C:用户 1 的更改 (营销) 显示。 放弃 User2 的更改 (服务) 。

在下一个方案中,User1 选择允许数据库值覆盖客户端中的当前值。 (请参阅本节后面的示例 3。)

在上述方案中,冲突解决后,数据库中的结果如下所示:

OverwriteCurrentValues

  Col A Col B Col C
OverwriteCurrentValues 阿尔弗雷德 (原始) Mary (User 2) 服务 (用户 2)
  • A:阿尔弗雷德) 保留 (原始值:放弃 User1 (Alfred) 的值。
  • Col B:用户 2 的更改 (Mary) 出现。
  • Col C:显示 User2 的更改 (服务) 。 用户 1 的更改 (市场营销) 将被丢弃。

解决冲突后,可以尝试重新提交。 由于第二次更新也可能失败,因此请考虑使用循环进行更新尝试。

示例

以下代码摘录显示了可用于发现和解决成员冲突的各种信息成员和技术。

示例 1

在此示例中,冲突是“自动”解决的。也就是说,数据库值将与当前客户端值合并,除非客户端也 (KeepChanges) 更改了该值。 不会对单个成员冲突进行检查或自定义处理。

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)

示例 2

在此示例中,无需任何自定义处理即可再次解决冲突。 但这次,数据库值不会合并到当前客户端值中。

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

示例 3

同样,不会发生任何自定义处理。 但在这种情况下,所有客户端值都使用当前数据库值进行更新。

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

示例 4

此示例演示了一种访问有关发生冲突的实体的信息的方法。

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

示例 5

此示例在各个成员中添加一个循环。 在这里,你可以提供任何成员的自定义处理。

注意使用 System.Reflection 添加;提供 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

存储过程调用

LINQ to SQL 支持存储过程和用户定义的函数。 LINQ to SQL 将这些数据库定义的抽象映射到代码生成的客户端对象,以便你可以从客户端代码以强类型的方式访问它们。 可以使用 IntelliSense 轻松发现这些方法,并且方法签名尽可能类似于数据库中定义的过程和函数的签名。 调用映射过程返回的结果集是强类型集合。 LINQ to SQL 可以自动生成映射的方法,但也支持在选择不使用代码生成的情况下手动映射。

LINQ to SQL 通过使用属性将存储过程和函数映射到方法。 StoredProcedureParameterFunction 属性都支持 Name 属性,Parameter 属性也支持 DBType 属性。 这里是两个示例:

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

以下示例演示各种存储过程的映射。

示例 1

以下存储过程采用单个输入参数并返回整数:

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

映射的方法如下所示:

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

示例 2

当存储过程可以返回多个结果形状时,返回类型无法强类型化为单个投影形状。 在以下示例中,结果形状取决于输入:

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

映射的方法如下所示:

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

可以使用此存储过程,如下所示:

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 
         
      }           

在这里,你需要根据对存储过程的了解,使用 GetResult 模式来获取正确类型的枚举器。 LINQ to SQL 可以生成所有可能的投影类型,但无法知道它们将按何种顺序返回。 知道哪些生成的投影类型对应于映射方法的唯一方法是对方法使用生成的代码注释。

示例 3

下面是 按顺序返回多个结果形状的存储过程的 T-SQL:

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

LINQ to SQL 将映射此过程,如上面的示例 2 所示。 但是,在这种情况下,有两个 连续 的结果集。

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

可以使用此存储过程,如下所示:

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

示例 4

LINQ to SQL 将参数映射到out引用参数 (ref 关键字 (keyword) ) ,对于值类型,将参数声明为可为 null (例如 int?) 。 以下示例中的过程采用单个输入参数并返回参数 out

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

映射的方法如下所示:

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

在这种情况下, 方法没有显式返回值,但默认返回值无论如何都映射。 对于输出参数,将按预期使用相应的输出参数。

可按如下所示调用上述存储过程:

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)

用户定义函数

LINQ to SQL 支持标量值函数和表值函数,并支持两者的内联对应函数。

LINQ to SQL 处理内联标量调用的方式类似于系统定义函数的调用方式。 请考虑下列查询:

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)

此处,方法调用 Math.Floor 转换为对系统函数 “FLOOR”的调用。 同样,对映射到 UDF 的函数的调用将转换为对 SQL 中的 UDF 的调用。

示例 1

下面是一个标量用户定义函数, (UDF) ReverseCustName () 。 在 SQL Server 中,函数的定义可能如下所示:

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

可以使用以下代码将架构类上定义的客户端方法映射到此 UDF。 请注意,方法主体构造一个表达式,该表达式捕获方法调用的意向,并将该表达式传递给 DataContext 以供转换和执行。 (仅当调用函数时才会发生此直接执行。)

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)

示例 2

在以下查询中,可以看到对生成的 UDF 方法 ReverseCustName 的内联调用。 在这种情况下,函数不会立即执行。 为此查询生成的 SQL 转换为对数据库中定义的 UDF 的调用, (查询) 后查看 SQL 代码。

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]

在查询 外部 调用同一函数时,LINQ to SQL 使用以下 SQL 语法从方法调用表达式创建一个简单的查询 (其中参数 @p0 绑定到) 中传递的常量:

在 LINQ to SQL 中:

C#

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

Visual Basic

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

转换为:

SELECT dbo.ReverseCustName(@p0)

示例 3

TVF) (表值函数返回单个结果集 (与存储过程不同,存储过程可以) 返回多个结果形状。 由于 TVF 返回类型是表,因此可以在 SQL 中使用表的任意位置使用 TVF,并且可以像对待表一样处理 TVF。

请考虑表值函数的以下SQL Server定义:

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

此函数显式声明它返回 TABLE,因此隐式定义返回的结果集结构。 LINQ to SQL 映射函数如下所示:

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)

以下 SQL 代码显示可以联接到函数返回的表,并像对待任何其他表一样对待它:

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

在 LINQ to SQL 中,查询将按如下方式呈现 (使用新的“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

针对存储过程的 LINQ to SQL 限制

LINQ to SQL 支持为返回静态确定的结果集的存储过程生成代码。 因此,LINQ to SQL 代码生成器不支持以下各项:

  • 使用动态 SQL 返回结果集的存储过程。 当存储过程包含用于生成动态 SQL 语句的条件逻辑时,LINQ to SQL 无法获取结果集的元数据,因为用于生成结果集的查询在运行时之前是未知的。
  • 基于临时表生成结果的存储过程。

实体类生成器工具

如果已有数据库,则无需手动创建完整的对象模型来表示它。 LINQ to SQL 分发版附带了一个名为 SQLMetal 的工具。 它是一个命令行实用工具,它通过从数据库元数据推断相应的类来自动执行创建实体类的任务。

可以使用 SQLMetal 从数据库中提取 SQL 元数据,并生成包含实体类声明的源文件。 或者,可以将该过程拆分为两个步骤,首先生成表示 SQL 元数据的 XML 文件,然后将该 XML 文件转换为包含类声明的源文件。 通过此拆分过程,可以将元数据保留为文件,以便对其进行编辑。 生成文件的提取过程在给定数据库的表名和列名的情况下,对适当的类和属性名称进行一些推理。 你可能会发现有必要编辑 XML 文件,以便生成器生成更令人愉悦的结果,或隐藏对象中不希望存在的数据库的各个方面。

使用 SQLMetal 的最简单方案是从现有数据库直接生成类。 下面介绍如何调用该工具:

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

执行该工具会创建一个 Northwind.cs.vb 文件,其中包含通过读取数据库元数据生成的对象模型。 如果数据库中表的名称与要生成的对象的名称相似,则此用法非常有效。 否则,需要采用两步方法。

若要指示 SQLMetal 生成 DBML 文件,请使用工具,如下所示:

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

生成 dbml 文件后,可以继续并使用 属性 属性对其进行批注,以描述表和列如何映射到类和属性。 对 dbml 文件进行批注后,可以通过运行以下命令来生成对象模型:

C#

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

Visual Basic

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

SQLMetal 用法签名如下所示:

SqlMetal [options] [filename]

下表显示了 SQLMetal 的可用命令行选项。

SQLMetal 的命令行选项

选项 说明
/server:<name> 指示为了访问数据库而要连接到的服务器。
/database:<name> 指示要从中读取元数据的数据库的名称。
/user:<name> 服务器的登录用户 ID。
/password:<name> 服务器的登录密码。
/views 提取数据库视图。
/functions 提取数据库函数。
/sprocs 提取存储过程。
/code[:<filename>] 指示该工具的输出是实体类声明的源文件。
/language:<language> 使用 Visual Basic 或 C# (默认) 。
/xml[:<filename>] 指示工具的输出是描述数据库元数据的 DBML 文件,以及类名和属性名称的第一个猜测近似值。
/map[:<filename>] 指示应使用外部映射文件而不是属性。
/pluralize 指示工具应对表的名称执行英语复数/反复数启发,以便生成适当的类和属性名称。
/namespace:<name> 指示将生成实体类的命名空间。
/timeout:<seconds> 用于数据库命令的超时值(以秒为单位)。

注意 若要从 MDF 文件中提取元数据,必须在所有其他选项之后指定 MDF 文件名。 如果未指定 /server ,则假定为 localhost

生成器工具 DBML 参考

DBML (数据库映射语言) 文件首先描述给定数据库的 SQL 元数据。 SQLMetal 通过查看数据库元数据来提取它。 SQLMetal 还使用相同的文件来生成表示数据库的默认对象模型。

下面是 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>

元素及其属性如下所述。

数据库

这是 XML 格式中最外层的元素。 此元素松散映射到生成的 DataContext 上的 Database 属性。

数据库属性

属性 类型 默认 说明
名称 String 数据库的名称。 如果存在,并且生成 DataContext,将使用此名称将数据库属性附加到该属性。 如果类属性不存在,也用作 DataContext 类的名称。
EntityNamespace 从 Table 元素中的 Type 元素生成的类的默认命名空间。 如果此处未指定命名空间,则会在根命名空间中生成实体类。
ContextNamespace 字符串 生成的 DataContext 类的默认命名空间。 如果此处未指定命名空间,则表示在根命名空间中生成 DataContext 类。
字符串 Database.Name 生成的 DataContext 类的名称。 如果不存在,请使用 Database 元素的 Name 属性。
AccessModifier AccessModifier 公用 生成的 DataContext 类的可访问性级别。 有效值为 PublicProtectedInternalPrivate
BaseType 字符串 “System.Data.Linq.DataContext” DataContext 类的基类型。
提供程序 字符串 “System.Data.Linq.SqlClient.Sql2005Provider” DataContext 的提供程序使用 Sql2005 提供程序作为默认提供程序
ExternalMapping 布尔 False 指定是否使用 DBML 生成外部映射文件。
序列化 SerializationMode SerializationMode.None 指定生成的 DataContext 和实体类是否可序列化。

数据库Sub-Element属性

Sub-Element 元素类型 出现次数范围 说明
<表> 0-无限制 表示将映射到单个类型或继承层次结构的SQL Server表或视图。
<Function> 函数 0-无限制 表示将映射到生成的 DataContext 类中方法的SQL Server存储过程或 db 函数。
<Connection> 连接 0-1 表示此 DataContext 将使用的数据库连接。

此元素表示将映射到单个类型或继承层次结构的数据库表 (或视图) 。 此元素松散映射到生成的实体类上的 Table 属性。

表属性

属性 类型 默认 说明
名称 String 所需的 () 数据库中表的名称。 根据需要用作表适配器的默认名称的基。
成员 字符串 Table.Name 在 DataContext 类中为此表生成的成员字段的名称。
AccessModifier AccessModifier 公用 DataContext表<T> 引用的可访问性级别。 有效值为 PublicProtectedInternalPrivate

表Sub-Element属性

Sub-Element 元素类型 出现范围 说明
类型<> 类型 1-1 表示映射到此表的类型或继承层次结构。
<InsertFunction> TableFunction 0-1 用于插入的方法。 如果存在,则会生成一个 InsertT 方法。
<UpdateFunction> TableFunction 0-1 用于更新的方法。 如果存在,则会生成方法 UpdateT
<DeleteFunction> TableFunction 0-1 用于删除的方法。 如果存在,则会生成一个 DeleteT 方法。

类型

此元素表示 Table 或存储过程结果形状的类型定义。 这将代码生成为具有指定列和关联的新 CLR 类型。

类型还可以表示继承层次结构的一个组件,其中多个类型映射到同一个表。 在这种情况下,Type 元素嵌套表示父子继承关系,并在数据库中通过指定的 InheritanceCode 进行区分。

类型属性

属性 类型 默认 说明
名称 String (必需) 要生成的 CLR 类型的名称。
InheritanceCode 字符串 如果此类型正在参与继承,则它可以具有关联的继承代码,以在从表中加载行时区分 CLR 类型。 其继承代码IsDiscriminator 列的值匹配的类型用于实例化加载的对象。 如果不存在继承代码,则生成的实体类是抽象的。
IsInheritanceDefault 布尔 False 如果继承层次结构中的 Type 为 true,则加载与任何定义的继承代码不匹配的行时,将使用此类型。
AccessModifier AccessModifier 公用 正在创建的 CLR 类型的辅助功能级别。 有效值为: PublicProtectedInternalPrivate
ID 字符串 类型可以具有唯一的 ID。类型 ID 可由其他表或函数使用。 ID 仅显示在 DBML 文件中,而不显示在对象模型中。
IdRef 字符串 IdRef 用于引用另一种类型的 ID。如果类型元素中存在 IdRef ,则类型元素必须仅包含 IdRef 信息。 IdRef 仅显示在 DBML 文件中,而不显示在对象模型中。

类型Sub-Element属性

Sub-Element 元素类型 出现范围 说明
<列> 0-unbounded 表示此类型中的属性,该属性将绑定到此类型的表中的字段。
<协会> 关联 0-unbounded 表示此类型中的一个属性,该属性将绑定到表之间的外键关系的一端。
类型<> 子类型 0-unbounded 表示继承层次结构中此类型的子类型。

子类型

此元素表示继承层次结构中的派生类型。 这将生成为具有此类型中指定的列和关联的新 CLR 类型。 不会为子类型生成继承属性。

Type 相比, SubType 元素没有 AccessModifier, 因为所有派生类型都必须是公共的。 子类型 不能被其他表和函数重用,因此它们中没有 IdIdRef

SubType 属性

属性 类型 默认 说明
名称 String 所需的 () 要生成的 CLR 类型的名称。
InheritanceCode 字符串 如果此类型正在参与继承,则它可以具有关联的继承代码,以在从表中加载行时区分 CLR 类型。 其继承代码IsDiscriminator 列的值匹配的类型用于实例化加载的对象。 如果不存在继承代码,则生成的实体类是抽象的。
IsInheritanceDefault 布尔 False 如果继承层次结构中的 Type 为 true,则加载与任何定义的继承代码不匹配的行时,将使用此类型。

SubType Sub-Element 属性

Sub-Element 元素类型 出现范围 说明
<列> 0-unbounded 表示此类型中的属性,该属性将绑定到此类型的表中的字段。
<协会> 关联 0-unbounded 表示此类型中的一个属性,该属性将绑定到表间外键关系的一端。
类型<> 子类型 0-unbounded 表示继承层次结构中此类型的子类型。

此元素表示表中的列,该列映射到类中的属性 (和后备字段) 。 但是,外键关系的任一端都不存在 Column 元素,因为关联元素) 两端完全表示 (。

列属性

属性 类型 默认 说明
名称 String 此列将映射到的数据库字段的名称。
成员 字符串 名称 要对包含的类型生成的 CLR 属性的名称。
存储 字符串 _成员 将存储此列值的专用 CLR 后备字段的名称。 序列化时不要删除 存储 ,即使它是默认的。
AccessModifier AccessModifier 公用 正在创建的 CLR 属性的辅助功能级别。 有效值为: PublicProtectedInternalPrivate
类型 字符串 (必需) 正在创建的 CLR 属性和后备字段的类型的名称。 这可以是从完全限定名称到类的直接名称的任何内容,只要该名称最终在编译生成的代码时位于范围内。
DbType 字符串 完整SQL Server类型 (包括此列的注释(如 NOT NULL) )。 如果提供 LINQ to SQL 用于优化生成的查询,并在执行 CreateDatabase () 时更具体。 始终序列化 DbType
IsReadOnly 布尔 False 如果设置了 IsReadOnly ,则不会创建属性集程序,这意味着用户无法使用该对象更改此列的值。
IsPrimaryKey 布尔 False 指示此列参与表的主键。 LINQ to SQL 需要此信息才能正常运行。
IsDbGenerated 布尔 False 指示此字段的数据由数据库生成。 这主要适用于 自动编号 字段和计算字段。 为这些字段赋值没有意义,因此它们自动为 IsReadOnly
CanBeNull 布尔 指示该值可以包含 null 值。 如果希望实际使用 CLR 中的 null 值,仍必须将 ClrType 指定为 可为 Null 的<T>
UpdateCheck UpdateCheck 始终 (,除非至少有另一个成员设置了 IsVersion ,否则从不) 指示 LINQ to SQL 在乐观并发冲突检测期间是否应使用此列。 默认情况下,除非存在 IsVersion 列,否则默认情况下,所有列都会参与其中。 可以是: AlwaysNeverWhenChanged (这意味着如果列自己的值) 更改,则列将参与其中。
IsDiscriminator 布尔 False 指示此字段是否包含用于在继承层次结构中的类型之间进行选择的鉴别器代码。
表达式 String 不会影响 LINQ to SQL 的操作,但在 期间使用 。CreateDatabase () 作为表示计算列表达式的原始 SQL 表达式。
IsVersion 布尔 False 指示此字段表示SQL Server中的 TIMESTAMP 字段,每次更改行时都会自动更新该字段。 然后,可以使用此字段实现更高效的乐观并发冲突检测。
IsDelayLoaded 布尔 False 指示不应在对象具体化后立即加载此列,而应仅在首次访问相关属性时加载。 这对于大型备忘录字段或并非总是需要的行中的二进制数据很有用。
AutoSync AutoSync 如果 (IsDbGenerated && IsPrimaryKey) OnInsert;

否则,如果 (IsDbGenerated) Always

否则从不

指定列是否从数据库生成的值自动同步。 此标记的有效值为: OnInsertAlwaysNever

关联

此元素表示外键关系的任一端。 对于一对多关系,一端为 EntitySet<T> ,多端为 EntityRef<T> 。 对于一对一关系,这两端均为 EntityRef<T>

请注意,不需要在关联的两端都有 关联 条目。 在这种情况下,仅在具有 条目的一侧生成属性 (形成单向关系) 。

关联属性

属性 类型 默认 说明
名称 String (必需) 关系的名称通常 (外键约束名称) 。 从技术上讲,这可以是可选的,但应始终由代码生成,以避免在同一两个表之间存在多个关系时出现歧义。
成员 字符串 名称 将在关联的这一端生成的 CLR 属性的名称。
存储 字符串 如果 OneToMany 和 Not IsForeignKey:

_OtherTable

还:

_TypeName (OtherTable)

将存储此列值的专用 CLR 支持字段的名称。
AccessModifier AccessModifier 公用 正在创建的 CLR 属性的可访问性级别。 有效值为 PublicProtectedInternalPrivate
ThisKey 字符串 包含类中的 IsIdentity 属性 关联的此端的键的逗号分隔列表。
OtherTable 字符串 请参阅说明。 关系另一端的表。 通常,这可以通过匹配关系名称由 LINQ to SQL 运行时确定,但对于单向关联或匿名关联来说,这是不可能的。
OtherKey 字符串 外类中的主键 关联另一端的键的逗号分隔列表。
IsForeignKey 布尔 False 指示这是否是关系的“子”端,即一对多的多端。
RelationshipType RelationshipType OneToMany 指示用户是断言与此关联相关的数据符合一对一数据的条件,还是符合一对多的更常规情况。 对于一对一,用户断言主键 (“one”) 端的每一行,外键 (“many”) 端只有一行。 这将导致在“一”端而不是 EntitySet<T 上生成 EntityRef T><。> 有效值为 OneToOneOneToMany
DeleteRule 字符串 用于向此关联添加删除行为。 例如,“CASCADE”会将“ONDELETECASCADE”添加到 FK 关系。 如果设置为 null,则不添加删除行为。

函数

此元素表示存储过程或数据库函数。 对于每个 Function 节点,都会在 DataContext 类中生成一个方法。

函数特性

属性 类型 默认 说明
名称 String (必需) 数据库中存储过程的名称。
方法 字符串 方法 要生成的允许调用存储过程的 CLR 方法的名称。 “方法”的默认名称已去除 [dbo]名称
AccessModifier AccessModifier 公用 存储过程方法的可访问性级别。 有效值为 PublicProtectedInternalPrivate
HasMultipleResults 布尔 类型 > 1 的数量 指定此 函数 节点表示的存储过程是否返回多个结果集。 每个结果集都是一个表格形状,可以是现有的 Type ,也可以是一组列。 在后一种情况下,将为列集创建 类型 节点。
IsComposable 布尔 False 指定函数/存储过程是否可以在 LINQ to SQL 查询中组合。 只能组合不返回 void 的数据库函数。

函数Sub-Element属性

Sub-Element 元素类型 出现次数范围 说明
<Parameter> 参数 0-无限制 表示此存储过程的 in 和 out 参数。
<ElementType> 类型 0-无限制 表示相应的存储过程可以返回的表格形状。
<Return> 返回 0-1 此 db 函数或存储过程的返回标量类型。 如果 Return 为 null,则函数返回 void。 函数不能同时具有 ReturnElementType

TableFunction

此元素表示表的 CUD 重写函数。 LINQ to SQL 设计器允许为 LINQ TO SQL 创建 InsertUpdateDelete 替代方法,并允许将实体属性名称映射到存储过程参数名称。

CUD 函数的方法名称是固定的,因此 DBML 中没有 TableFunction 元素的 Method 属性。 例如,对于 Customer 表,CUD 方法命名为 InsertCustomerUpdateCustomerDeleteCustomer

表函数无法返回表格形状,因此 TableFunction 元素中没有 ElementType 属性。

TableFunction 属性

属性 类型 默认 说明
名称 String (必需) 数据库中存储过程的名称。
AccessModifier AccessModifier 私有 存储过程方法的可访问性级别。 有效值为 PublicProtectedInternalPrivate
HasMultipleResults 布尔 类型 > 1 的数量 指定此函数节点表示的存储过程是否返回多个结果集。 每个结果集都是一个表格形状,可以是现有的 Type ,也可以是一组列。 在后一种情况下,将为列集创建 类型 节点。
IsComposable 布尔 False 指定函数/存储过程是否可以在 LINQ to SQL 查询中组合。 只能组合不返回 void 的数据库函数。

TableFunction Sub-Element 特性

Sub-Elements 元素类型 出现次数范围 说明
<Parameter> TableFunctionParameter 0-无限制 表示此表函数的 in 和 out 参数。
<Return> TableFunctionReturn 0-1 此表函数返回的标量类型。 如果 Return 为 null,则函数返回 void。

参数

此元素表示存储过程/函数参数。 参数可以传入和传出数据。

参数属性

属性 类型 默认 说明
名称 String (必需) 存储的 proc/function 参数的数据库名称。
参数 字符串 名称 方法参数的 CLR 名称。
  字符串 (必需) 方法参数的 CLR 名称。
DbType 字符串 存储的 proc/function 参数的 DB 类型。
方向 ParameterDirection 位于 参数流动的方向。 可以是 InOutInOut 之一。

返回

此元素表示存储过程/函数的返回类型。

返回属性

属性 类型 默认 说明
类型 字符串 (必需) 存储的过程/函数结果的 CLR 类型。
DbType 字符串 存储的过程/函数结果的数据库类型。

TableFunctionParameter

此元素表示 CUD 函数的参数。 参数可以传入和传出数据。每个参数都映射到此 CUD 函数所属的 Table 列。 此元素中没有 TypeDbType 属性,因为可以从参数映射到的列获取类型信息。

TableFunctionParameter 属性

属性 类型 默认 说明
名称 String (必需) CUD 函数参数的数据库名称。
参数 字符串 名称 方法参数的 CLR 名称。
字符串 名称 此参数映射到的列名。
方向 ParameterDirection 位于 参数流动的方向。 可以是 InOutInOut 之一。
版本 版本 当前 PropertyName 是引用给定列的当前版本还是原始版本。 仅在 更新 替代期间适用。 可以是 “当前”“原始”。

TableFunctionReturn

此元素表示 CUD 函数的返回类型。 实际上,它只包含映射到 CUD 函数结果的列名称。 可以从 列获取返回的类型信息。

TableFunctionReturn 属性

Attrobite 类型 默认 说明
字符串 返回要映射到的列名。

连接

此元素表示默认的数据库 连接 参数。 这允许为已知道如何连接到数据库的 DataContext 类型创建默认构造函数。

有两种类型的默认连接可能,一种使用直接 ConnectionString,另一种从 App.Settings 读取

连接属性

属性 类型 默认 说明
UseApplicationSettings 布尔 False 确定是使用 App.Settings 文件还是从直接 ConnectionString 获取应用程序设置
ConnectionString 字符串 要发送到 SQL 数据提供程序的连接字符串。
SettingsObjectName 字符串 设置 要从中检索属性的 App.Settings 对象
SettingsPropertyName 字符串 ConnectionString 包含 ConnectionStringApp.Settings 属性。

多层实体

在两层应用程序中,单个 DataContext 处理查询和更新。 但是,对于具有其他层的应用程序,通常需要使用单独的 DataContext 实例进行查询和更新。 例如,如果 ASP.NET 应用程序,则对 Web 服务器的单独请求执行查询和更新。 因此,跨多个请求使用相同的 DataContext 实例是不切实际的。 在这种情况下, DataContext 实例需要能够更新它尚未检索的对象。 LINQ to SQL 中的多层实体支持通过 Attach () 方法提供此类功能。

下面是如何使用其他 DataContext 实例更改 Customer 对象的示例:

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()

在多层应用程序中,出于简单性、互操作性或隐私性,通常不会跨层发送整个实体。 例如,供应商可能会为 Web 服务定义不同于中间层使用的 Order 实体的数据协定。 同样,网页可能只显示 Employee 实体成员的子集。 因此,多层支持旨在适应此类情况。 只有属于以下一个或多个类别的成员需要在层之间传输,并在调用 Attach () 之前进行设置。

  1. 属于实体标识的成员。
  2. 已更改的成员。
  3. 参与乐观并发检查的成员。

如果时间戳或版本号列用于乐观并发检查,则必须在调用 Attach () 之前设置相应的成员。 在调用 Attach () 之前,无需设置其他成员的值。 LINQ to SQL 使用具有乐观并发检查的最少更新;也就是说,忽略未设置或检查乐观并发的成员。

可以使用 LINQ to SQL API 范围之外的各种机制保留乐观并发检查所需的原始值。 ASP.NET 应用程序可以使用视图状态 (或使用视图状态) 的控件。 Web 服务可将 DataContract 用于更新方法,以确保原始值可用于更新处理。 为了互操作性和通用性,LINQ to SQL 不规定层之间交换的数据的形状或用于往返原始值的机制。

用于插入和删除的实体不需要 Attach () 方法。 用于两层应用程序(Table.Add () Table.Remove () )的方法可用于插入和删除。 与两层更新一样,用户负责处理外键约束。 如果数据库中存在外键约束阻止删除具有订单的客户,则不能仅删除具有订单的客户而不处理其订单。

LINQ to SQL 还以传递方式处理实体的附件,以便进行更新。 用户基本上根据需要创建更新前对象图,并调用 Attach () 。 然后,可以在附加的图上“重播”所有更改,以完成必要的更新,如下所示:

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()

外部映射

除了基于属性的映射外,LINQ to SQL 还支持外部映射。 外部映射最常见的形式是 XML 文件。 映射文件支持需要将映射与代码分离的其他方案。

DataContext 提供了用于提供 MappingSource 的附加构造函数。 MappingSource 的一种形式是从 XML 映射文件构造的 XmlMappingSource

下面是如何使用映射文件的示例:

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   )

下面是映射文件中的对应代码片段,其中显示了 Product 类的映射。 它显示了映射到 Northwind 数据库中的 Products 表的命名空间中的 Product映射。 元素和属性与属性名称和参数一致。

<?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>

NET Framework 函数支持和说明

以下段落提供有关 LINQ to SQL 类型支持以及.NET Framework差异的基本信息。

基元类型

实现

  • 算术运算符和比较运算符
  • Shift 运算符: << 和 >>
  • 字符和数值之间的转换由 UNICODE/NCHAR 完成;否则使用 SQL 的 CONVERT

未实现

  • <类型>。解析
  • 可以使用枚举并将其映射到表中的整数和字符串。 对于后者,使用 ParseToString () 方法。

与 .NET 的差异

  • ToString for double 的输出在 SQL 上使用 CONVERT (NVARCHAR (30) , @x2) ,它始终使用 16 位数字和“科学表示法”。例如:“0.000000000000000000e+000”表示 0,因此它不会提供与 相同的字符串。NET 的 Convert.ToString ()

System.String

实现

  • 非静态方法:

    • LengthSubstringContainsStartsWithEndsWithIndexOfInsertRemoveReplaceTrimToLowerToUpperLastIndexOfPadRightPadLeftEqualsCompareTo。 支持所有签名,除非它们采用 StringComparison 参数,等等,如下所述。
  • 静态方法:

       Concat(...)               all signatures
       Compare(String, String)
       String (indexer) 
       Equals(String, String)
    
  • 构造 函数:

        String(Char, Int32)
    
  • 运营商:

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

未实现

  • 采用或生成 char 数组的方法。

  • 采用 CultureInfo/StringComparison/IFormatProvider 的方法。

  • 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
    
  • 实例:

       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)
    

限制/与 .NET 的差异

SQL 使用排序规则来确定字符串的相等性和顺序。 可以在SQL Server实例、数据库、表列或表达式上指定这些值。

到目前为止实现的函数的转换不会更改排序规则,也不会在已翻译的表达式上指定不同的排序规则。 因此,如果默认排序规则不区分大小写, 则 CompareToIndexOf 等函数可能会提供与 .NET 函数) (区分大小写的结果不同。

方法 StartsWith (str) /EndsWith (str) 假定参数 str 是客户端上计算的常量或表达式。 也就是说,目前无法将列用于 str

System.Math

已实现静态方法

  • 所有签名:
    • AbsAcosAsinAtanAtan2BigMulCeilingCosCos、CoshExpFloorLogLog10MaxMinPowSignSinhSqrtTanTanhTruncate

未实现

  • IEEERemainder
  • DivRem 有一个 out 参数,因此不能在表达式中使用该参数。 常量 Math.PIMath.E 在客户端上进行评估,因此不需要转换。

与 .NET 的差异

.NET 函数 Math.Round 的转换是 SQL 函数 ROUND。 仅当指定了指示 MidpointRounding 枚举值的重载时,才支持转换。 MidpointRounding.AwayFromZero 是 SQL 行为,MidpointRounding.ToEven 指示 CLR 行为。

System.Convert

实现

  • 窗体 To<Type1> 的方法 (<Type2> x) 其中 Type1、Type2 是以下类型之一:
    • bool字节charDateTimedecimaldoublefloatInt16Int32Int64string
  • 该行为与强制转换相同:
    • 对于 ToString (Double) 有特殊代码来获取完全精度。
    • 对于 转换 Int32/Char,LINQ to SQL 使用 SQL 的 UNICODE/NCHAR 函数。
    • 否则,转换为 CONVERT

未实现

  • ToSByteUInt163264:SQL 中不存在这些类型。

    To<integer type>(String, Int32) 
    ToString(..., Int32)       any overload ending with an Int32 toBase
    IsDBNull(Object)
    GetTypeCode(Object)
    ChangeType(...)
    
  • 具有 IFormatProvider 参数的版本。

  • 涉及数组的方法 (To/FromBase64CharArray, To/FromBase64String) 。

System.TimeSpan

实现

  • 构造函数:

       TimeSpan(Long)
       TimeSpan (year, month, day)
       TimeSpan (year, month, day, hour, minutes, seconds)
       TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
    
  • 运营商:

       Comparison operators: <,==, and so on in C#; <, =, and so on in Visual Basic
    
       +, -
    
  • Visual Basic 中的静态 (共享) 方法:

       Compare(t1,t2)
    
  • 非静态 (实例) 方法/属性:

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

未实现

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

System.DateTime

实现

  • 构造函数:

       DateTime(year, month, day)
       DateTime(year, month, day, hour, minutes, seconds)
       DateTime(year, month, day, hour, minutes, seconds, milliseconds)
    
  • 运营商:

       Comparisons
       DateTime – DateTime (gives TimeSpan)
       DateTime + TimeSpan (gives DateTime)
       DateTime – TimeSpan (gives DateTime)
    
  • 静态 (共享) 方法:

       Add(TimeSpan), AddTicks(Long),
       AddDays/Hours/Milliseconds/Minutes (Double)
       AddMonths/Years(Int32)
       Equals
    
  • 非静态 (实例) 方法/属性:

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

与 .NET 的差异

SQL 的日期/时间值四舍五入为 .000、.003 或 .007 秒,因此其精度低于 .NET 的值。

SQL 的日期时间范围从 1753 年 1 月 1 日开始。

SQL 没有 适用于 TimeSpan 的内置类型。 它使用不同的 DATEDIFF 方法返回 32 位整数。 一个是 DATEDIFF (DAY,...) ,它给出天数;另一个是 DATEDIFF (MILLISECOND,...) ,它提供毫秒数。 如果 DateTime 相 隔超过 24 天,则会导致错误。 相比之下,.NET 使用 64 位整数,以刻度为单位测量 TimeSpans

为了尽可能接近 SQL 中的 .NET 语义,LINQ to SQL 将 TimeSpans 转换为 64 位整数,并使用上面提到的两个 DATEDIFF 方法计算两个日期之间的刻度数。

Datetime 将查询转换为 (时,将在客户端上计算 UtcNow,就像不涉及数据库数据) 的任何表达式一样。

未实现

   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

调试支持

DataContext 提供方法和属性来获取为查询和更改处理生成的 SQL。 这些方法可用于了解 LINQ to SQL 功能和调试特定问题。

用于获取生成的 SQL 的 DataContext 方法

成员 目的
日志 在执行 SQL 之前打印 SQL。 涵盖查询、插入、更新、删除命令。 用法:

C#

db.Log = Console.Out;

Visual Basic

db.Log = Console.Out

GetQueryText (查询) 返回查询的查询文本,而不执行它。 用法:

C#

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

Visual Basic

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

GetChangeText () 返回不执行插入/更新/删除的 SQL 命令的文本。 用法:

C#

Console.WriteLine(db.GetChangeText());

Visual Basic

Console.WriteLine(db.GetChangeText())