LINQ to SQL:基本概念和功能
LINQ to SQL 的基本概念和功能,包括使用 L2S 查询和更新 SQL 数据库,延迟执行、延迟加载和预先加载。
引言
在我发表在 CodeProject 上的前三篇文章中,我解释了 Windows Communication Foundation (WCF) 的基础知识,包括
从上个月开始,我开始撰写一些文章来解释 LINQ、LINQ to SQL、Entity Framework 和 LINQ to Entities。以下是我撰写或计划撰写关于 LINQ、LINQ to SQL 和 LINQ to Entities 的文章:
- LINQ 简介——语言集成查询(上一篇文章)
- LINQ to SQL:基本概念与功能(本文)
- LINQ to SQL:高级概念与功能(下一篇文章)
- LINQ to Entities:基本概念与功能(未来文章)
- LINQ to Entities:高级概念与功能(未来文章)
完成这五篇文章后,我将回来撰写更多关于 WCF 的文章,这些文章将来自我的实际工作经验,如果您现在正在使用 WCF,这些文章肯定会对您的实际工作有所帮助。
概述
在上一篇文章中,我们学习了 C# 3.0 中 LINQ 的一些新功能。在本文和下一篇文章中,我们将了解如何使用 LINQ 与 SQL Server 数据库进行交互,换句话说,如何在 C# 中使用 LINQ to SQL。
在本文中,我们将介绍 LINQ to SQL 的基本概念和功能,其中包括:
- 什么是 ORM
- 什么是 LINQ to SQL
- 什么是 LINQ to Entities
- 比较 LINQ to SQL 与 LINQ to Objects 和 LINQ to Entities
- 在 LINQ to SQL 中建模 Northwind 数据库
- 使用表查询和更新数据库
- 延迟执行
- 延迟加载和预先加载
- 连接两个表
- 使用视图查询
在下一篇文章中,我们将介绍 LINQ to SQL 的高级概念和功能,例如存储过程支持、继承、同步更新和事务处理。
ORM——对象关系映射
LINQ to SQL 被认为是微软新的 ORM 产品之一。因此,在我们开始解释 LINQ to SQL 之前,让我们先了解一下什么是 ORM。
ORM 代表对象关系映射(Object-Relational Mapping)。有时也称为 O/RM 或 O/R 映射。它是一种编程技术,包含一组类,用于将关系数据库实体映射到特定编程语言中的对象。
最初,应用程序可以调用指定的原生数据库 API 来与数据库通信。例如,Oracle Pro*C 是 Oracle 提供的一组 API,用于从 C 应用程序中查询、插入、更新或删除 Oracle 数据库中的记录。Pro*C 预编译器将嵌入式 SQL 翻译成对 Oracle 运行时库 (SQLLIB) 的调用。
随后,ODBC(开放数据库连接)被开发出来,以统一各种 RDBMS 的所有通信协议。ODBC 旨在独立于编程语言、数据库系统和操作系统。因此,通过 ODBC,应用程序可以使用相同的代码与不同的 RDBMS 进行通信,只需替换底层的 ODBC 驱动程序即可。
无论使用哪种方法连接到数据库,从数据库返回的数据都必须以某种格式呈现在应用程序中。例如,如果从数据库返回一个订单记录,则必须有一个变量来保存订单号,以及一组变量来保存订单详细信息。或者,应用程序可以为订单创建一个类,为订单详细信息创建另一个类。当开发另一个应用程序时,可能需要再次创建相同的类集,或者如果设计得当,它们可以放入库中并被各种应用程序重用。
这正是 ORM 的用武之地。通过 ORM,每个数据库都由特定编程语言中的 ORM 上下文对象表示,数据库实体(如表)由类表示,这些类之间存在关系。例如,ORM 可以创建一个 `Order` 类来表示订单表,并创建一个 `OrderDetail` 类来表示订单详细信息表。`Order` 类将包含一个集合成员来保存其所有详细信息。ORM 负责这些类与数据库之间的映射和连接。因此,对于应用程序来说,数据库现在完全由这些类表示。应用程序只需要处理这些类,而无需处理物理数据库。应用程序无需担心如何连接到数据库,如何构建 SQL 语句,如何使用适当的锁定机制来确保并发性,或者如何处理分布式事务。这些与数据库相关的活动由 ORM 处理。
下图展示了从应用程序访问数据库的三种不同方式。还有其他机制可以从应用程序访问数据库,例如 JDBC 和 ADO.NET。但是,为了保持图表简单,此处未显示它们。
LINQ to SQL
LINQ to SQL 是 .NET Framework 3.5 版本的一个组件,它提供了一个运行时基础设施,用于将关系数据作为对象进行管理。
在 LINQ to SQL 中,关系数据库的数据模型被映射到用开发人员的编程语言表达的对象模型。当应用程序运行时,LINQ to SQL 将对象模型中的语言集成查询转换为 SQL 并将其发送到数据库执行。当数据库返回结果时,LINQ to SQL 将它们转换回对象,您可以在自己的编程语言中操作这些对象。
LINQ to SQL 完全支持事务、视图、存储过程和用户定义函数。它还提供了一种将数据验证和业务逻辑规则集成到数据模型中的简便方法,并支持对象模型中的单表继承。
LINQ to SQL 是 Microsoft 新的 ORM 产品之一,旨在与市场上许多现有的 .NET 平台 ORM 产品竞争,例如开源产品 NHibernate、NPersist,以及商业产品 LLBLGen 和 WilsonORMapper。LINQ to SQL 与其他 ORM 产品有许多重叠之处,但由于它是专门为 .NET 和 SQL Server 设计和构建的,因此它比其他 ORM 产品具有许多优势。例如,它利用了所有 LINQ 功能,并完全支持 SQL Server 存储过程。您可以获得所有表的所有关系(外键),并且每个表的字段都成为其相应对象的属性。当您输入实体(表)名称时,甚至会出现智能感知弹出窗口,列出数据库中的所有字段。此外,所有字段和查询结果都是强类型的,这意味着如果您拼错查询语句或将查询结果转换为错误的类型,您将收到编译错误而不是运行时错误。此外,由于它是 .NET Framework 的一部分,您无需在生产和开发环境中安装和维护任何第三方 ORM 产品。
LINQ to SQL 的底层,ADO.NET SqlClient 适配器用于与真实的 SQL Server 数据库通信。我们将在本文后面看到如何在运行时捕获生成的 SQL 语句。
下图展示了 LINQ to SQL 在 .NET 应用程序中的使用方式:
我们将在本文和后续文章中详细探讨 LINQ to SQL 的功能。
比较 LINQ to SQL 与 LINQ to Objects
在上一篇文章中,我们使用 LINQ 查询内存中的对象。在我们深入 LINQ to SQL 的世界之前,我们首先来看看 LINQ to SQL 和 LINQ to Objects 之间的关系。
以下是 LINQ to SQL 和 LINQ to Objects 之间的一些主要区别:
- LINQ to SQL 需要一个 Data Context 对象。Data Context 对象是 LINQ 和数据库之间的桥梁。LINQ to Objects 不需要任何中间 LINQ 提供程序或 API。
- LINQ to SQL 返回 `IQueryable
` 类型的数据,而 LINQ to Objects 返回 `IEnumerable ` 类型的数据。 - LINQ to SQL 通过表达式树转换为 SQL,这使得它们可以作为一个单元进行评估,并转换为适当且优化的 SQL 语句。LINQ to Objects 不需要转换。
- LINQ to SQL 被转换为 SQL 调用并在指定的数据库上执行,而 LINQ to Objects 在本地机器内存中执行。
LINQ 各个方面共有的相似之处在于语法。它们都使用相同的类似 SQL 的语法并共享相同的标准查询运算符组。从语言语法角度来看,处理数据库与处理内存中的对象是相同的。
LINQ to Entities
对于 LINQ to SQL,您想要比较的另一个产品是 .NET Entity Framework。在比较 LINQ to SQL 和 Entity Framework 之前,让我们先看看 Entity Framework 是什么。
ADO.NET Entity Framework (EF) 首次随 Visual Studio 2008 和 .NET Framework 3.5 Service Pack 1 发布。到目前为止,许多人将 EF 视为 Microsoft 的另一个 ORM 产品,尽管从设计上讲,它应该比仅仅是一个 ORM 工具强大得多。
使用 Entity Framework,开发人员使用概念数据模型(实体数据模型或 EDM),而不是底层数据库。概念数据模型架构用概念架构定义语言(CSDL)表达,实际存储模型用存储架构定义语言(SSDL)表达,两者之间的映射用映射架构语言(MSL)表达。为这个新框架创建了一个新的数据访问提供程序 `EntityClient`,但在底层,ADO.NET 数据提供程序仍然用于与数据库通信。下图取自 2008 年 7 月的 MSDN Magazine,展示了 Entity Framework 的架构。
从图中可以看出,LINQ 是可以用来查询 Entity Framework 实体的一种查询语言。LINQ to Entities 允许开发人员使用 LINQ 表达式和 LINQ 标准查询运算符,针对实体数据模型 (EDM) 创建灵活、强类型的查询。它与 LINQ to SQL 所能做的一样,尽管 LINQ to Entities 比 LINQ to SQL 支持更多的功能,例如多表继承,并且它支持除 Microsoft SQL Server 之外的许多其他主流 RDBMS 数据库,如 Oracle、DB2 和 MySQL。
比较 LINQ to SQL 和 LINQ to Entities
如前所述,LINQ to Entities 应用程序针对概念数据模型 (EDM) 工作。语言和数据库之间的所有映射都通过新的 `EntityClient` 映射提供程序进行。应用程序不再直接连接到数据库或看到任何特定于数据库的构造;整个应用程序都以更高级别的 EDM 模型进行操作。
这意味着您不能再使用原生数据库查询语言;数据库不仅无法理解 EDM 模型,而且当前的数据库查询语言也不具备处理 EDM 引入的元素(如继承、关系、复杂类型等)所需的构造。
另一方面,对于不需要映射到概念模型的开发人员来说,LINQ to SQL 使开发人员能够直接在现有数据库架构上体验 LINQ 编程模型。
LINQ to SQL 允许开发人员生成表示数据的 .NET 类。这些生成的类不映射到概念数据模型,而是直接映射到数据库表、视图、存储过程和用户定义函数。使用 LINQ to SQL,开发人员可以使用与先前描述的内存集合、实体或 DataSet 以及 XML 等其他数据源相同的 LINQ 编程模式,直接针对存储架构编写代码。
与 LINQ to Entities 相比,LINQ to SQL 有一些限制,主要是因为它直接映射到物理关系存储架构。例如,您不能将两个不同的数据库实体映射到一个单独的 C# 或 VB 对象,并且底层数据库架构的更改可能需要对客户端应用程序进行重大更改。
总之,如果您想针对概念数据模型工作,请使用 LINQ to Entities。如果您想从编程语言直接映射到数据库,请使用 LINQ to SQL。
下表列出了这两种数据访问方法支持的一些功能
特点 |
LINQ to SQL |
LINQ to Entities |
概念数据模型 |
否 |
是 |
存储架构 |
否 |
是 |
映射架构 |
否 |
是 |
新的数据访问提供程序 |
否 |
是 |
支持非 SQL Server 数据库 |
否 |
是 |
直接数据库连接 |
是 |
否 |
语言扩展支持 |
是 |
是 |
存储过程 |
是 |
是 |
单表继承 |
是 |
是 |
多表继承 |
否 |
是 |
来自多个表的单个实体 |
否 |
是 |
支持延迟加载 |
是 |
是 |
我们将在本文中使用 LINQ to SQL,因为我们将在数据访问层中使用它,而数据访问层只是 WCF 服务的三个层之一。LINQ to SQL 比 LINQ to Entities 简单得多,因此我们仍然可以在同一篇文章中将其与 WCF 一起介绍。然而,一旦您通过本文学习了如何使用 LINQ to SQL 开发 WCF 服务,并通过其他方式学习了如何使用 LINQ to Entities,您就可以轻松地将您的数据访问层迁移到使用 LINQ to Entities。
创建 LINQ to SQL 测试应用程序
现在我们已经学习了 LINQ to SQL 的一些基本概念,接下来让我们通过实际示例来探索 LINQ to SQL。
首先,我们需要创建一个新项目来测试 LINQ to SQL。我们将重用在上一篇文章(LINQ 简介——语言集成查询)中创建的解决方案。如果您没有阅读那篇文章,您可以直接从该文章下载源文件,或者创建一个新的解决方案 TestLINQ。
您还需要安装包含 Northwind 示例数据库的 SQL Server 数据库。您可以搜索“Northwind 示例数据库下载”,然后下载并安装该示例数据库。如果您需要关于如何下载/安装示例数据库的详细说明,您可以参考我之前的一篇文章《使用 Entity Framework 实现 WCF 服务》中的“准备数据库”部分。
现在按照以下步骤向解决方案添加新应用程序
- 打开(或创建)解决方案 TestLINQ。
- 从解决方案资源管理器中,右键单击解决方案项,然后从上下文菜单中选择“添加 | 新建项目...”。
- 选择“Visual C# | Windows”作为项目类型,选择“控制台应用程序”作为项目模板,输入“TestLINQToSQLApp”作为(项目)名称,并输入“D:\SOAwithWCFandLINQ\Projects\TestLINQ\TestLINQToSQLApp”作为位置。
- 点击“确定”。
Northwind 数据库建模
接下来要做的是对 Northwind 数据库进行建模。我们现在将 Northwind 数据库中的两个表和一个视图拖放到我们的项目中,以便稍后我们可以使用它们来演示 LINQ to SQL。
将 LINQ to SQL 项添加到项目
首先,让我们向项目 TestLINQToSQLApp 添加一个新项。新添加的项应为 LINQ to SQL 类类型,并命名为 Northwind,如以下“添加新项”对话框窗口所示。
点击“添加”按钮后,以下三个文件将被添加到项目中:Northwind.dbml、Northwind.dbml.layout 和 Northwind.designer.cs。第一个文件保存数据库模型的设计界面,而第二个文件是模型的 XML 格式。在 Visual Studio IDE 中只能打开其中一个。第三个是模型的代码隐藏,它定义了模型的 DataContext
。
此时,Visual Studio LINQ to SQL 设计器应该已打开并为空,如下图所示:
连接到 Northwind 数据库
现在我们需要连接到 Northwind 示例数据库,以便从数据库中拖放对象。
- 从 IDE 最左侧打开“服务器资源管理器”窗口。您可以将鼠标悬停在“服务器资源管理器”上并等待一秒钟,或者单击“服务器资源管理器”将其打开。如果它在您的 IDE 中不可见,请选择菜单“视图 | 服务器资源管理器”,或按 Ctrl+Alt+S 将其打开。
- 在服务器资源管理器中,右键单击“数据连接”,然后选择“添加连接”以打开添加连接窗口。在此窗口中,指定您的服务器名称(如果不是默认安装,请包括您的实例名称)、登录信息,并选择 Northwind 作为数据库。您可以单击“测试连接”按钮以确保所有设置正确。
- 单击“确定”添加此连接。从现在开始,Visual Studio 将使用此数据库作为项目的默认数据库。您可以查看新文件 Properties\Settings.Designer.cs 以获取更多信息。
向设计图面添加表和视图
新连接 Northwind.dbo 现在应该出现在服务器资源管理器中。接下来,我们将两个表和一个视图拖放到 LINQ to SQL 设计图面上。
- 展开连接直到列出所有表,然后将 Products 拖到 Northwind.dbml 设计图面上。您应该会看到如下图所示的屏幕:
- 然后将 Categories 表从服务器资源管理器拖到 Northwind.dbml 设计图面上。
- 我们还需要使用视图查询数据,因此将 Current Product List 视图从服务器资源管理器拖到 Northwind.dbml 设计图面上。
屏幕上的 Northwind.dbml 设计图面应如下图所示:
生成的 LINQ to SQL 类
如果您打开 Northwind.Designer.cs 文件,您会发现为项目生成了以下类:
public partial class NorthwindDataContext : System.Data.Linq.DataContext
public partial class Product : INotifyPropertyChanging, INotifyPropertyChanged
public partial class Category : INotifyPropertyChanging, INotifyPropertyChanged
public partial class Current_Product_List
在上述四个类中,DataContext
类是我们从数据库中查询实体并将其更改应用回数据库的主要渠道。它包含各种类型和构造函数、部分验证方法以及所有包含的表的属性成员。它继承自 System.Data.Linq.DataContext
类,该类表示 LINQ to SQL 框架的主要入口点。
接下来的两个类是我们感兴趣的两个表的类。它们都实现了 INotifyPropertyChanging
和 INotifyPropertyChanged
接口。这两个接口定义了所有相关的属性更改和属性已更改事件方法,我们可以扩展这些方法以在更改前后验证属性。
最后一个类用于视图。它是一个简单的类,只有两个属性成员。由于我们不打算通过此视图更新数据库,因此它不定义任何属性更改或已更改事件方法。
使用表查询和更新数据库
现在我们已经创建了实体类,我们将使用它们与数据库进行交互。我们将首先处理产品表,查询、更新记录,以及插入和删除记录。
查询记录
首先,我们将查询数据库以获取一些产品。
要使用 LINQ to SQL 查询数据库,我们首先需要构建一个 `DataContext` 对象,如下所示:
NorthwindDataContext db = new NorthwindDataContext();
然后我们可以使用这个 LINQ 查询语法从数据库中检索记录
IEnumerable<Product> beverages = from p in db.Products
where p.Category.CategoryName == "Beverages"
orderby p.ProductName
select p;
上述代码将检索“饮料”类别中所有按产品名称排序的产品。
更新记录
我们可以更新我们刚刚从数据库中检索到的任何产品,如下所示:
// update one product
Product bev1 = beverages.ElementAtOrDefault(10);
if (bev1 != null)
{
Console.WriteLine("The price of {0} is {1}. Update to 20.0",
bev1.ProductName, bev1.UnitPrice);
bev1.UnitPrice = (decimal)20.00;
}
// submit the change to database
db.SubmitChanges();
我们使用了 ElementAtOrDefault
方法,而不是 ElementAt
方法,以防在第 10 个元素处没有产品。不过,在示例数据库中,有 12 种饮料产品,第 11 种(从索引 0 开始的第 10 个元素)是 Steeleye Stout,其单价为 18.00。我们将其价格更改为 20.00,并调用 db.SubmitChanges()
以更新数据库中的记录。运行程序后,如果您查询 ProductID 为 35 的产品,您会发现其价格现在为 20.00。
插入记录
我们还可以创建一个新产品,然后将此新产品插入数据库,如以下代码所示:
Product newProduct = new Product {ProductName="new test product" };
db.Products.InsertOnSubmit(newProduct);
db.SubmitChanges();
删除记录
要删除一个产品,我们首先需要从数据库中检索它,然后只需调用 `DeleteOnSubmit` 方法,如以下代码所示:
// delete a product
Product delProduct = (from p in db.Products
where p.ProductName == "new test product"
select p).FirstOrDefault();
if(delProduct != null)
db.Products.DeleteOnSubmit(delProduct);
db.SubmitChanges();
运行程序
目前 `Program.cs` 文件如下所示。请注意,我们将 `db` 声明为类成员,并添加了一个方法来包含所有表操作的测试用例。我们将添加更多方法来测试其他 LINQ to SQL 功能。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
namespace TestLINQToSQLApp
{
class Program
{
// create data context
static NorthwindDataContext db = new NorthwindDataContext();
static void Main(string[] args)
{
// CRUD operations on tables
TestTables();
Console.ReadLine();
}
static void TestTables()
{
// retrieve all Beverages
IEnumerable<Product> beverages = from p in db.Products
where p.Category.CategoryName == "Beverages"
orderby p.ProductName
select p;
Console.WriteLine("There are {0} Beverages", beverages.Count());
// update one product
Product bev1 = beverages.ElementAtOrDefault(10);
if (bev1 != null)
{
Console.WriteLine("The price of {0} is {1}. Update to 20.0",
bev1.ProductName, bev1.UnitPrice);
bev1.UnitPrice = (decimal)20.0;
}
// submit the change to database
db.SubmitChanges();
// insert a product
Product newProduct = new Product { ProductName = "new test product" };
db.Products.InsertOnSubmit(newProduct);
db.SubmitChanges();
Product newProduct2 = (from p in db.Products
where p.ProductName == "new test product"
select p).SingleOrDefault();
if (newProduct2 != null)
{
Console.WriteLine("new product inserted with product ID {0}",
newProduct2.ProductID);
}
// delete a product
Product delProduct = (from p in db.Products
where p.ProductName == "new test product"
select p).FirstOrDefault();
if (delProduct != null)
{
db.Products.DeleteOnSubmit(delProduct);
}
db.SubmitChanges();
}
}
}
如果您运行该程序,输出将是:
延迟执行
在使用 LINQ to SQL 时,一个重要的注意事项是 LINQ 的延迟执行。
标准查询运算符的执行时间因其返回单个值还是值序列而异。返回单个值的方法(例如 `Average` 和 `Sum`)会立即执行。返回序列的方法会延迟查询执行并返回一个可枚举对象。这些方法直到查询对象被枚举时才消耗目标数据。这被称为延迟执行。
对于操作内存中集合的方法,即那些扩展 `IEnumerable
相比之下,扩展 `IQueryable
使用 SQL Profiler 检查延迟执行
有两种方法可以查看查询何时执行。第一种方法是打开 Profiler(所有程序\Microsoft SQL Server 2005(或 2008)\性能工具\SQL 2005(或 2008)Profiler);启动对 Northwind 数据库引擎的新跟踪,然后调试程序。例如,当执行以下语句时,Profiler 中没有任何内容:
IEnumerable<Product> beverages = from p in db.Products
where p.Category.CategoryName == "Beverages"
orderby p.ProductName
select p;
然而,当执行以下语句时,从 profiler 中,您将看到数据库中执行了一个查询:
Console.WriteLine("There are {0} Beverages", beverages.Count());
在数据库中执行的查询如下所示:
exec sp_executesql N'SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID],
[t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice],
[t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued]
FROM [dbo].[Products] AS [t0]
LEFT OUTER JOIN [dbo].[Categories] AS [t1] ON [t1].[CategoryID] = [t0].[CategoryID]
WHERE [t1].[CategoryName] = @p0
ORDER BY [t0].[ProductName]',N'@p0 nvarchar(9)',@p0=N'Beverages'
Profiler 窗口应如下图所示:
从 Profiler 中,我们知道 LINQ 在底层实际调用了 `sp_executesql`,并且它还使用左外连接来获取产品的类别。
使用 SQL 日志检查延迟执行
跟踪 LINQ 语句执行时间的另一种方法是使用日志。`DataContext` 类提供了一个方法来记录它执行的每个 SQL 语句。要查看日志,我们首先可以在程序开头,`Main` 之后立即添加此语句:
db.Log = Console.Out;
然后我们可以在变量 `beverages` 定义之后,但在引用其 `Count` 之前添加此语句:
Console.WriteLine("After query syntax is defined, before it is referenced.");
因此,前几行语句现在如下所示:
static void Main(string[] args)
{
// log database query statements to stand out
db.Log = Console.Out;
// CRUD operations on tables
TestTables();
Console.ReadLine();
}
static void TestTables()
{
// retrieve all Beverages
IEnumerable<Product> beverages = from p in db.Products
where p.Category.CategoryName == "Beverages"
orderby p.ProductName
select p;
Console.WriteLine("After query syntax beverages is defined, " +
"before it is referenced.");
Console.WriteLine("There are {0} Beverages", beverages.Count());
// rest of the file
现在,如果您运行程序,输出将是这样的:
从日志中,我们看到查询在定义查询语法时并未执行。相反,它在调用 `beverages.Count()` 时执行。
单例方法的延迟执行
但是,如果查询表达式将返回一个单例值,则查询将在定义时立即执行。例如,我们可以添加此语句以获取所有产品的平均价格:
decimal? averagePrice = (from p in db.Products
select p.UnitPrice).Average();
Console.WriteLine("After query syntax averagePrice is defined, before it is referenced.");
Console.WriteLine("The average price is {0}", averagePrice);
输出如下:
从这个输出中,我们知道查询在定义查询语法的同时执行。
序列表达式中单例方法的延迟执行
然而,仅仅因为查询使用了 `Sum`、`Average` 或 `Count` 等单例方法,并不意味着查询在定义时就会执行。如果查询结果是一个序列,执行仍然会被延迟。以下是这种查询的一个示例:
// deferred execution2
var cheapestProductsByCategory =
from p in db.Products
group p by p.CategoryID into g
select new
{
CategoryID = g.Key,
CheapestProduct =
(from p2 in g
where p2.UnitPrice == g.Min(p3 => p3.UnitPrice)
select p2).FirstOrDefault()
};
Console.WriteLine("Cheapest products by category:");
foreach (var p in cheapestProductsByCategory)
{
Console.WriteLine("categery {0}: product name: {1} price: {2}",
p.CategoryID, p.CheapestProduct.ProductName, p.CheapestProduct.UnitPrice);
}
如果您运行上述查询,您会发现它是在打印结果时执行的,而不是在定义查询时执行的。部分结果如下所示:
从这个输出中,您可以看到当结果被打印时,它首先去数据库获取每个类别的最低价格,然后对于每个类别,它再次去数据库获取具有该价格的第一个产品。尽管在实际产品中,您可能不希望在应用程序代码中编写如此复杂的查询,而是将其放在存储过程中。
延迟(懒惰)加载与预先加载
在上面一个例子中,我们通过这个表达式检索了产品的类别名称:
p.Category.CategoryName == "Beverages"
尽管产品表中没有名为类别名称的字段,我们仍然可以获取产品的类别名称,因为产品表和类别表之间存在关联。在 Northwind.dbml 设计图面上,点击产品表和类别表之间的连线,您将看到该关联的所有属性。请注意,其参与属性是 Category.CategoryID -> Product.CategoryID,这意味着类别 ID 是链接这两个表的关键字段。
由于这种关联,我们可以为每个产品检索类别,另一方面,我们也可以为每个类别检索产品。
默认懒惰加载
然而,即使有此关联,关联数据在查询执行时也不会加载。例如,如果像这样检索所有类别:
var categories = from c in db.Categories select c;
稍后,如果我们需要获取每个类别的产品数量,则必须再次查询数据库。此图显示了查询的执行结果:
从这张图可以看出,LINQ 首先查询数据库获取所有类别,然后对于每个类别,当我们需要获取产品总数时,它会再次查询数据库以获取该类别的所有产品。
这是因为默认情况下,延迟加载(lazy loading)设置为 true,这意味着所有关联数据(子项)都将延迟加载,直到需要时才加载。
使用加载选项预先加载
为了改变这种行为,我们可以使用 `LoadWith` 方法来告诉 `DataContext` 在初始查询中自动加载指定的子项,如下所示:
// eager loading products of categories
DataLoadOptions dlo2 = new DataLoadOptions();
dlo2.LoadWith<Category>(c => c.Products);
// create another data context, because we can't change LoadOptions of db
// once a query has been executed against it
NorthwindDataContext db2 = new NorthwindDataContext();
db2.Log = Console.Out;
db2.LoadOptions = dlo2;
var categories2 = from c in db2.Categories select c;
foreach (var category2 in categories2)
{
Console.WriteLine("There are {0} products in category {1}",
category2.Products.Count(), category2.CategoryName);
}
db2.Dispose();
注意:`DataLoadOptions` 位于 `System.Data.Linq` 命名空间中,因此您必须在程序中添加 `using` 语句。
using System.Data.Linq;
此外,我们必须为这个测试创建一个新的 `DataContext` 实例,因为我们已经对原始 `db` `DataContext` 运行了一些查询,并且不再可能更改其 `LoadOptions`。
现在类别加载后,其所有子项(产品)也将加载。这可以从下图中得到证明:
从这张图可以看出,所有类别的所有产品都在第一次查询中加载。
使用加载选项进行过滤加载
虽然 `LoadWith` 用于预先加载所有子项,但 `AssociateWith` 可用于筛选要加载的子项。例如,如果我们只想加载类别 1 和 2 的产品,我们可以编写如下查询:
// eager loading only certain children
DataLoadOptions dlo3 = new DataLoadOptions();
dlo3.AssociateWith<Category>(
c => c.Products.Where(p => p.CategoryID == 1 || p.CategoryID == 2));
// create another data context, because we can't change LoadOptions of db
// once query has been executed against it
NorthwindDataContext db3 = new NorthwindDataContext();
db3.LoadOptions = dlo3;
db3.Log = Console.Out;
var categories3 = from c in db3.Categories select c;
foreach (var category3 in categories3)
{
Console.WriteLine("There are {0} products in category {1}",
category3.Products.Count(), category3.CategoryName);
}
db3.Dispose();
现在,如果我们查询所有类别并打印出每个类别的产品数量,我们会发现只有前两个类别包含产品,所有其他类别根本没有产品,如下图所示:
结合预先加载和过滤加载
然而,从上面的输出可以看出,这是延迟加载。如果您想预先加载带有一些过滤条件的产品,您可以将 `LoadWith` 和 `AssociateWith` 结合起来,如下面的代码所示:
DataLoadOptions dlo4 = new DataLoadOptions();
dlo4.LoadWith<Category>(c => c.Products);
dlo4.AssociateWith<Category>(c => c.Products.Where(
p => p.CategoryID == 1 || p.CategoryID == 2));
// create another data context, because we can't change LoadOptions of db
// once q query has been executed
NorthwindDataContext db4 = new NorthwindDataContext();
db4.Log = Console.Out;
db4.LoadOptions = dlo4;
var categories4 = from c in db4.Categories select c;
foreach (var category4 in categories4)
{
Console.WriteLine("There are {0} products in category {1}",
category4.Products.Count(), category4.CategoryName);
}
db4.Dispose();
输出如下图所示:
注意,对于实体中的每个字段,您还可以设置其“延迟加载”属性来改变其加载行为。这与子级延迟/预先加载不同,因为它只影响该特定实体的一个属性。
连接两个表
虽然关联是一种连接,但在 LINQ 中,我们也可以使用 `Join` 关键字显式连接两个表,如以下代码所示:
var categoryProducts =
from c in db.Categories
join p in db.Products on c.CategoryID equals p.CategoryID into products
select new {c.CategoryName, productCount = products.Count()};
foreach (var cp in categoryProducts)
{
Console.WriteLine("There are {0} products in category {1}",
cp.CategoryName, cp.productCount);
}
在上面的例子中它不是很有用,因为 Products 表和 Categories 表通过外键关系关联。当两个表之间没有外键关联时,这将特别有用。
从输出中,我们可以看到只执行了一个查询来获取结果。
除了连接两个表,您还可以连接三个或更多表,自连接,创建左/右外连接,或使用复合键连接。
使用视图查询
使用视图查询与使用表查询相同。例如,您可以像这样调用视图“current product lists”:
var currentProducts = from p in db.Current_Product_Lists
select p;
foreach (var p in currentProducts)
{
Console.WriteLine("Product ID: {0} Product Name: {1}",
p.ProductID, p.ProductName);
}
这将使用视图获取所有当前产品。
摘要
在本文中,我们学习了什么是 ORM,为什么我们需要 ORM,以及什么是 LINQ to SQL。我们还比较了 LINQ to SQL 和 LINQ to Entities,并探讨了 LINQ to SQL 的一些基本功能。
本文的要点包括:
- ORM 产品可以大大简化数据访问层开发。
- LINQ to SQL 是 Microsoft 的 ORM 产品之一,用于对 SQL Server 数据库使用 LINQ。
- Visual Studio 2008 中内置的 LINQ to SQL 设计器可用于数据库建模。
- 您可以在 Visual Studio 2008 服务器资源管理器中连接到数据库,然后将数据库项拖放到 LINQ to SQL 设计图面上。
- 类 `System.Data.Linq.DataContext` 是 LINQ to SQL 应用程序的主要类。
- 返回序列的 LINQ 方法会延迟查询执行,您可以使用 Profiler 或 SQL 日志检查执行时间。
- 返回单例值的 LINQ 查询表达式将在定义时立即执行。
- 默认情况下,关联数据是延迟(懒惰)加载的。您可以使用 `LoadWith` 选项更改此行为。
- 关联数据的结果可以使用 `AssociateWith` 选项进行筛选。
- `LoadWith` 和 `AssociateWith` 选项可以组合使用,以预先加载带筛选条件的关联数据。
- `Join` 运算符可用于连接多个表和视图。
- 在 LINQ to SQL 中,视图可以像表一样用于查询数据库。
注意:本文基于我的旧书《WCF Multi-tier Services Development with LINQ》(ISBN 1847196624)的第 10 章。由于 LINQ to SQL 现在已不再是微软首选,本书已在我的新书《WCF 4.0 Multi-tier Services Development with LINQ to Entities》(ISBN 1849681147)中升级为使用 LINQ to Entities。这两本书都是关于如何在 Microsoft 平台上构建 SOA 应用程序的实践指南,旧书在 Visual Studio 2008 中使用 WCF 和 LINQ to SQL,新书在 Visual Studio 2010 中使用 WCF 和 LINQ to Entities。
无论选择哪本书,您都可以通过完成实际示例并将其应用于您的实际工作,学习如何掌握 WCF 和 LINQ to SQL/LINQ to Entities 的概念。它们是少数几本将 WCF 和 LINQ to SQL/LINQ to Entities 结合到多层实际 WCF 服务中的书籍。它们非常适合希望学习如何构建可扩展、强大、易于维护的 WCF 服务的初学者。两本书都包含丰富的示例代码、清晰的解释、有趣的示例和实用的建议。它们是真正的 C++ 和 C# 开发人员的实践书籍。
您不需要有 WCF 或 LINQ to SQL/LINQ to Entities 的经验即可阅读这两本书。详细的说明和精确的屏幕截图将引导您完成探索 WCF 和 LINQ to SQL/LINQ to Entities 新世界的整个过程。这两本书与其他 WCF 和 LINQ to SQL/LINQ to Entities 书籍的区别在于,它们侧重于如何做,而不是为什么这样做,因此您不会被大量关于 WCF 和 LINQ to SQL/LINQ to Entities 的信息所淹没。一旦您读完其中一本书,您会为自己以最直接的方式使用 WCF 和 LINQ to SQL/LINQ to Entities 而感到自豪。
您可以从亚马逊(搜索 WCF 和 LINQ)或出版商网站购买这两本书:https://www.packtpub.com/wcf-4-0-multi-tier-services-development-with-linq-to-entities/book。