LINQ 离线模式的 ASP.NET 数据层基类






4.73/5 (20投票s)
使用此抽象类快速轻松地实现您的 LINQ 数据层
引言
LINQ to SQL 是 .NET 3.0 的一项绝佳补充!它为 .NET 应用程序中的数据访问提供了一种类型安全、强大且极其灵活的实现方式。
然而,在多层数据库应用程序中使用 LINQ to SQL 并不像直接使用那么简单。本文将介绍使用 LINQ to SQL 实现数据层时常见的陷阱,并提供一种简单、方便且灵活的方法来规避其中大多数问题。
本文提供的 LINQ to SQL 数据库抽象层 (DAL) 的通用基类具有以下特性:
- 实现了 Repository 模式,让您能够方便地为每个 LINQ 实体类型编写少于十行代码的 CRUD (
Create
,Update
,Delete
) 操作。 - 在离线 LINQ 模式下无缝工作。
- 支持在一次数据库往返中,透明地更新 LINQ 实体层次结构。
- 作为一个便利功能,它还会在调试应用程序时,将所有执行的 SQL 语句写入输出控制台。
必备组件
本文假设您对 LINQ to SQL (也称为 DLINQ) 的功能及其使用方法有基本了解。如果您不了解,请先阅读 本教程,然后再返回此页面学习如何在多层应用程序中使用 LINQ to SQL。
问题
如果您直接将 UI 层与数据库通过 LINQDataSource
对象连接起来,LINQ to SQL 将会非常容易使用。但这种方式并不怎么面向对象,肯定也不是一个推荐的架构。除非您正在编写一个快速粗糙的应用程序,并且不打算长期扩展它。
相反,大多数开发人员会将应用程序划分为多个层,例如以下结构:
- 数据访问层
- 业务层
- UI 层
这被称为多层数据库应用程序设计。LINQ to SQL 将用于数据访问层。
LINQ to SQL 的问题在于——尽管它有许多优点——但在实现数据层时,它并不那么容易使用。
请看以下数据库模式:

只要您将 LINQ 实体加载到同一个数据上下文实例并从中保存(这称为“联机模式”),使用 LINQ 实现数据层就会非常直接。
例如,我们从数据库中获取 ID 为 1 的客户实体,将其名字更改为“Homer”,然后将其保存回数据库。在多层数据库应用程序中,这段代码可能位于 UI 或业务层,并且会像这样:
//create a new repository instance
CustomersRepository customersRepository = new CustomersRepository();
//load a customer instance and change its FirstName
Customer customer = customersRepository.Load(2);
customer.FirstName = "Homer";
//commit customer to database
customersRepository.Save(customer);
实现上面使用的 Load
和 Save
数据层函数的简便方法是这样的:
static DataClassesDataContext context=new DataClassesDataContext();
public Customer Load(int CustomerID)
{
return context.Customers.Single(c => c.ID == CustomerID);
}
public void Save(Customer toSave)
{
context.SubmitChanges();
}
这种方法使用联机 LINQ 模式:数据上下文永远不会超出范围,因此总可以重用它来将仍然连接到它的实体保存到数据库。
诚然,这很方便,并且对于上面孤立的示例有效。但是,它存在严重的并发问题,因为一个数据库上下文用于所有数据库操作:调用 Save()
时,SubmitChanges
会提交所有已更改的实体,而不仅仅是 Save
方法在 toSave
参数中接收的 LINQ 实体。
但是,即使撇开这个缺点不谈,在多层 ASP.NET 应用程序中使用 LINQ 时,您也无法以同样的方式实现数据层。在这种情况下,您的 LINQ 实体很可能是在页面请求中加载的,然后在下一个页面请求中进行更新并保存到数据库。在此期间,您原来的数据上下文已经超出了范围,使得您的 LINQ 实体处于断开连接状态。
而且,在许多其他场景下都需要使用断开连接的 LINQ 模式:例如,您可能希望将数据库层实现为 Web 服务,提交先前序列化的 LINQ 实体到您的数据库,等等。
使用断开连接的 LINQ 实现数据层
那么,如何实现一个可以在断开连接的 LINQ 模式下工作的 Save()
数据层方法呢?
我们必须:
- 将实体从旧的数据上下文中分离
- 创建一个新的数据上下文
- 将实体附加到新上下文中
- 提交更改
在源代码中,它看起来像这样:
public Customer Load(int CustomerID)
{
DataClassesDataContext context =new DataClassesDataContext();
return context.Customers.Single(c => c.ID == CustomerID);
}
public void Save(Customer toSave)
{
//the old data context is no more, we need to create a new one
DataClassesDataContext context = new DataClassesDataContext();
//serialize and deserialize the entity to detach it from the
//old data context. This is not part of .NET, I am calling
//my own code here
toSave = EntityDetacher<Customer>.Detach(toSave);
//is the entity new or just updated?
//ID is the customer table's identity column, so new entities should
//have an ID == 0
if (toSave.ID==0)
{
//insert entity into Customers table
context.Customers.InsertOnSubmit(toSave);
}
else
{
//attach entity to Customers table and mark it as "changed"
context.Customers.Attach(toSave,true);
}
}
现在,您可以加载和修改任意数量的实体,并且只提交其中一些到数据库。但是,由于使用了断开连接的 LINQ,这种实现没有考虑 LINQ 实体之间的关联。
例如,假设您想在业务层或 UI 层中执行以下操作:
//load currently selected customer from database
Customer customer = new CustomersRepository().Load(1);
//change the customer's first name
customer.FirstName = "Homer";
//add a new bill with two billingitems to the customer
Bill newbill = new Bill
{
Date = DateTime.Now,
BillingItems =
{
new BillingItem(){ItemPrice=10, NumItems=2},
new BillingItem(){ItemPrice=15, NumItems=1}
}
};
customer.Bills.Add(newbill);
//create a new provider to simulate new ASP.NET page request
// save the customer
new CustomersRepository().Save(customer);
上面断开连接模式的 Save()
方法将提交对 FirstName
列的更改,但会忽略新的账单和账单项。为了使其正常工作,我们还需要递归地 Attach
或 Insert
所有关联的子实体。
public void Save(Customer toSave)
{
//the old data context is no more, we need to create a new one
DataClassesDataContext context = new DataClassesDataContext();
//serialize and deserialize the entity to detach it from the
//old data context. This is not part of .NET, I am calling
//my own code here
toSave = EntityDetacher<customer>.Detach(toSave);
//is the entity new or just updated?
//ID is the customer table's identity column, so new entities should
//have an ID == 0
if (toSave.ID==0)
{
//insert entity into Customers table
context.Customers.InsertOnSubmit(toSave);
}
else
{
//attach entity to Customers table and mark it as "changed"
context.Customers.Attach(toSave,true);
}
//attach or save all "bill" child entities
foreach (Bill bill in toSave.Bills)
{
if (bill.ID == 0)
{
context.Bills.InsertOnSubmit(bill);
}
else
{
context.Bills.Attach(bill, true);
}
//attach or save all "BillingItem" child entities
foreach (BillingItem billingitem in bill.BillingItems)
{
if (bill.ID == 0)
{
context.BillingItems.InsertOnSubmit(billingitem);
}
else
{
context.BillingItems.Attach(billingitem, true);
}
}
}
}
这并不复杂,但那只是针对一个简单的数据库模式和一个实体类型。想象一下,如果您正在为几十种实体类型实现数据库层,并且有几十个外键关系。您将不得不为每个需要 DAL Repository 类的 LINQ 实体编写几十个嵌套的 foreach
循环。这不仅乏味,而且容易出错。每当您添加一个新表时,您都需要向各种 DAL Repository 类添加几十个 foreach
循环。
解决方案:RepositoryBase
我实现了一个名为 RepositoryBase
的类,您可以使用它来快速实现适用于上述示例的数据层。
为了使用它,您必须首先指示对象关系映射器生成可序列化的 LINQ 实体:在 Visual Studio 中打开您的 DBML 文件,在空白区域单击鼠标左键,然后在“属性”面板中将“序列化模式”设置为“单向”。

现在,您可以从 RepositoryBase
派生来实现自己的 Repository:
public class CustomersRepository :
//derive from RepositoryBase with the entity name and
//data context as generic parameters
DeverMind.RepositoryBase<Customer, DataClassesDataContext>
{
override protected Expression<Func<Customer, bool>> GetIDSelector(int ID)
{
//ID needs to be the entity's ID column name
return (Item) => Item.ID == ID;
}
}
public partial class Customer
{
public static RepositoryBase<Customer,DataClassesDataContext> CreateRepository()
{
//create and return an instance of this entity type's repository
return new CustomersRepository();
}
}
为您的每种实体类型执行此操作,您就拥有了一个在断开连接模式下无缝工作的数据层。您的派生 Repository
类将自动实现以下方法:

作为小福利,在调试应用程序时,您还可以通过 ProviderBase
在调试输出控制台中看到针对数据库运行的 SQL 命令。
没有免费午餐……
Load
操作没有显著的性能损失,但调用 Save
或 Delete
方法时,后台会进行一些反射操作。
对于绝大多数 DAL 需求,这可能不会对您的应用程序产生显著影响。然而,如果您执行大量更新/插入/删除操作,特别是涉及大量嵌套子实体时,您可能希望为这些子应用程序的 Repository
类编写自己的 Save
/ Delete
函数,如上所述。所有 Save
/ Delete
函数都是 virtual
的,因此您可以轻松地重写它们。
另外,请注意 RepositoryBase
不支持带循环依赖的递归保存或删除操作。
结论
本文和随附的源代码提供了一种简单、方便且可扩展的方法来实现您的多层 LINQ 数据层 CRUD 方法。它可以在断开连接模式下工作,并支持嵌套子实体的保存和加载。在保存和加载操作上存在轻微的性能损失,但您可以为这些性能至关重要的 Repository 重写它们。对于其他所有情况,只需几行代码即可搞定。
如果您有任何疑问,请告诉我。此外,欢迎随时访问我的 博客,了解更多开发文章。
版本历史
- 2008 年 10 月 7 日 - V.0.1
- 首次发布
- 2008 年 2 月 26 日 - V.0.2
RepositoryBase
现在会更新已保存实体的 ID 和版本属性。- 增加了对多个 ID 列的支持。
谢谢!
感谢 Kris Vandermotten 提供的便捷 DebuggerWriter
组件,RepositoryBase
使用它来输出 SQL 调试信息。