LINQ to SQL实现数据访问通用基类

开发 后端 数据库运维
LINQ to SQL让人着迷,在.Net应用程序当中,.它提供了一种安全,强大和非常灵活的方式执行数据访问,在当前微软传道者介绍上看,很容易上手。

不幸的是,当你对LINQ进行仔细研究后,我发现在多层架构中使用LINQ的并不是十分容易。

本文介绍用LINQ to SQL实现数据层的典型的问题点 ,并提供了一个简单,方便和灵活的方式来克服它们。

本文附带的LINQ to SQL 实现数据访问通用类有以下的特点:

实现了存储库模式,你可以用不到10行代码执行LINQ实体类型的CRUD (Create, Update, Delete)操作。
无缝协作,支持LINQ断开模式(Disconnected LINQ Mode)。
在单一数据库和LINQ实体间支持透明的数据库更新和数据加载。
提供为一种方便的功能,在调试你的应用程寻时候,它把所有执行的SQL语句输出控制台。
本文将假定您对LINQ to SQL (也称为DLINQ )有一个基本的了解并如何使用它。否则,,,回到此网页,看看本教程入门系列,如何在多层次应用中使用LINQ to SQL。

存在的问题

如果您只是在你的UI层直接用LinqToDataSource对象衔接数据库,那LINQ to SQL太容易使用了。但是,这种做法不完全面向对象,当然也不是一个可取的架构,除非你是为了快速编码和脏乱的应用程序,并且最终没有去扩展的它打算。

相反,大多数开发人员把它们的应用程序划分成若干层,如下:

数据访问层(Data Access Layer)

业务层 (Business Layer)

用户界面层(UI Layer)

这就是所谓的多层数据库应用程序设计。LINQ to SQL将用于数据访问层。

LINQ to SQL的问题是-尽管它的许多优点-但是如果要实现数据层并不是很简单。

请看下面的数据库模式(database schema):

一旦你要加载和保存LINQ实体到同一个的数据上下文实例(data context instance)(这就是所谓“连接模式”),用LINQ实现数据层非常直接。

例如,让我们从数据库中获取实体编号为1的客户,改变属性first name为“Homer”后在重新储存到数据库中。在一个多层数据库应用程序中,在UI或业务层的某个地方的代码可能看起来就像这样:

view plaincopy to clipboardprint?
1.         
2.       //create a new repository instance   
3.       CustomersRepository customersRepository = new CustomersRepository();   
4.       //load a customer instance and change it's FirstName;   
5.       Customer customer = customersRepository.Load(2);   
6.       customer.FirstName = "Homer";   
7.       //commmit customer to database   
8.       customersRepository.Save(customer);  
 

最简单的方法来实现上面使用到的数据层加载和保存功能是:

view plaincopy to clipboardprint?
1.         
2.       static DataClassesDataContext context=new DataClassesDataContext();   
3.       public Customer Load(int CustomerID)   
4.       {   
5.       return context.Customers.Single(c => c.ID == CustomerID);   
6.       }   
7.       public void Save(Customer toSave)   
8.       {   
9.       context.SubmitChanges();   
10.   }  

这种方法是使用连接LINQ模式:数据上下文(data context)在当前作用域一直有效(译者注:一直保持连接状态),所以在把实体保存到数据库的时候,它总是可以重复使用。其中仍然连接到它。

当然,这种做法方便并且在上述的单个例子中能运行,但它存在严重的并发问题,因为一个数据库方面是用于所有数据库操作。

当调用方法Save(),bmitChanges提交的不仅仅是当前Save 方法参数相关的LINQ实体,还包括所有改变了的实体。

但是及时把这个缺陷考虑在一边,使用LINQ在一个多层ASP.NET应用程序中,您还不能以相同方式实现数据层。首先,可能要求是这样,在一个页面请求中,LINQ实体被加载,然后在下一个页面请求中,它更新并储存到数据库中的。.同时,您的原始数据上下文在当前作用域内已经无效的(译者住:HTTP协议是无状态的),造成的您的LINQ实体游离。

还有许多其他情况下你需要使用断开LINQ模式:例如您实现的数据库层可能要作为一个Web服务,提交(commit)以前序列化LINQ实体到数据库等等。

用断开模式的LINQ to SQL实现数据访问层

所以,在断开的LINQ模式下,我们如何实现数据层的Save( )方法?

我们必须

Detach the entity from the old data context从旧的数据上下文中分离实体

Create a new data context创建一个新的数据上下文

Attach the entity to the new context附加实体到新的数据上下文

Submit changes提交更改

在源代码,它看起来像这样:

 view plaincopy to clipboardprint?
1.         
2.       public Customer Load(int CustomerID)   
3.       {   
4.       DataClassesDataContext context = new DataClassesDataContext();   
5.       return context.Customers.Single(c => c.ID == CustomerID);   
6.       }   
7.         
8.       public void Save(Customer toSave)   
9.       {   
10.   //the old data context is no more, we need to create a new one   
11.   DataClassesDataContext context = new DataClassesDataContext();   
12.   //serialize and deserialize the entity to detach it from the   
13.   //old data context. This is not part of .NET, I am calling   
14.   //my own code here   
15.   toSave = EntityDetacher.Detach(toSave);   
16.   //is the entity new or just updated?   
17.   //ID is the customer table's identity column, so new entities should   
18.   //have an ID == 0   
19.   if (toSave.ID == 0)   
20.   {   
21.   //insert entity into Customers table   
22.   context.Customers.InsertOnSubmit(toSave);   
23.   }   
24.   else  
25.   {   
26.   //attach entity to Customers table and mark it as "changed"   
27.   context.Customers.Attach(toSave, true);   
28.   }   
29.   }  
 

现在只要你喜欢,您可以加载修改任意多实体,并且只提交他们一部分到数据库。但由于使用断开的LINQ ,这个程序并不会感知到LINQ实体之间的关系。

例如,假设在业务层或用户界面层您要做到以下几点:

view plaincopy to clipboardprint?
1.         
2.       //load currently selected customer from database   
3.       Customer customer = new CustomersRepository().Load(1);   
4.       //change the customer's first name   
5.       customer.FirstName = "Homer";   
6.       //add a new bill with two billingitems to the customer   
7.       Bill newbill = new Bill   
8.       {   
9.       Date = DateTime.Now,   
10.   BillingItems =   
11.   {   
12.   new BillingItem(){ItemPrice=10, NumItems=2},   
13.   new BillingItem(){ItemPrice=15, NumItems=1}   
14.   }   
15.   };   
16.   customer.Bills.Add(newbill);   
17.   //create a new provider to simulate new ASP.NET page request   
18.   //save the customer   
19.   new CustomersRepository().Save(customer);  
 

这个断开模式下,上述Save( )方法将提交变更到FirstName列,但是忽略了new bill和billing items。为了做到这一点,我们还需要附加或插入递归所有相关的子实体(child entities):

 

view plaincopy to clipboardprint?

1.        

2.       public void Save(Customer toSave)  

3.       {  

4.       //the old data context is no more, we need to create a new one  

5.       DataClassesDataContext context = new DataClassesDataContext();  

6.       //serialize and deserialize the entity to detach it from the  

7.       //old data context. This is not part of .NET, I am calling  

8.       //my own code here  

9.       toSave = EntityDetacher.Detach(toSave);  

10.   //is the entity new or just updated?  

11.   //ID is the customer table's identity column, so new entities should  

12.   //have an ID == 0  

13.   if (toSave.ID == 0)  

14.   {  

15.   //insert entity into Customers table  

16.   context.Customers.InsertOnSubmit(toSave);  

17.   }  

18.   else 

19.   {  

20.   //attach entity to Customers table and mark it as "changed"  

21.   context.Customers.Attach(toSave, true);  

22.   }  

23.   //attach or save all "bill" child entities  

24.   foreach (Bill bill in toSave.Bills)  

25.   {  

26.   if (bill.ID == 0)  

27.   {  

28.   context.Bills.InsertOnSubmit(bill);  

29.   }  

30.   else 

31.    

32.   {  

33.   context.Bills.Attach(bill, true);  

34.   }  

35.   //attach or save all "BillingItem" child entities  

36.   foreach (BillingItem billingitem in bill.BillingItems)  

37.   {  

38.   if (bill.ID == 0)  

39.   {  

40.   context.BillingItems.InsertOnSubmit(billingitem);  

41.   }  

42.   else 

43.   {  

44.   context.BillingItems.Attach(billingitem, true);  

45.   }  

46.   }  

47.   }  

48.   } 

不是很复杂,但很多打字(译者注:翻译不是很难,但要一句句的理解,还要打很多字)。并且这只是支持一个微不足道的database scheme和一个单一的实体类型。.想象一下,如果实现数据库层有几十个实体类型与几十个外键关系,在这个数据存储类中,你将要为每一个LINQ实体写几十套foreach循环,这不仅是单调乏味,而且还容易出错。.当你添加新的表,你必须添加几十foreach循环。

如何避免这些问题

在相当多的在线调研后,我实现了一个RepositoryBase类,使用他您可以快速实现您的数据层,所示为测试通过的例子。 首先,用对象关系映射器(译者注:Visual Studio自带工具)来产生序列化的LINQ实体:在Visual Studio中打开dbml文件,在空白区域某处左键单击,弹出属性窗口,设置“Serialization Mode属性”为“Unidirectional”。

 

现在您可以继承RepositoryBase实现您自己的Repository:

view plaincopy to clipboardprint?
1.         
2.       public void Save(Customer toSave)   
3.       {   
4.       //the old data context is no more, we need to create a new one   
5.       DataClassesDataContext context = new DataClassesDataContext();   
6.       //serialize and deserialize the entity to detach it from the   
7.       //old data context. This is not part of .NET, I am calling   
8.       //my own code here   
9.       toSave = EntityDetacher.Detach(toSave);   
10.   //is the entity new or just updated?   
11.   //ID is the customer table's identity column, so new entities should   
12.   //have an ID == 0   
13.   if (toSave.ID == 0)   
14.   {   
15.   //insert entity into Customers table   
16.   context.Customers.InsertOnSubmit(toSave);   
17.   }   
18.   else  
19.   {   
20.   //attach entity to Customers table and mark it as "changed"   
21.   context.Customers.Attach(toSave, true);   
22.   }   
23.   //attach or save all "bill" child entities   
24.   foreach (Bill bill in toSave.Bills)   
25.   {   
26.   if (bill.ID == 0)   
27.   {   
28.   context.Bills.InsertOnSubmit(bill);   
29.   }   
30.   else  
31.     
32.   {   
33.   context.Bills.Attach(bill, true);   
34.   }   
35.   //attach or save all "BillingItem" child entities   
36.   foreach (BillingItem billingitem in bill.BillingItems)   
37.   {   
38.   if (bill.ID == 0)   
39.   {   
40.   context.BillingItems.InsertOnSubmit(billingitem);   
41.   }   
42.   else  
43.   {   
44.   context.BillingItems.Attach(billingitem, true);   
45.   }   
46.   }   
47.   }   
48.   }  

您的每一个实体的类型都照这样做,你就拥有了一个工作在断开模式下无缝数据层。您继承Repository的类自动执行下列方法:

 

 


作为锦上添花的功能,在应用程序调试的过程中,你还可以通过输出控制台看到执行对数据库的操作的SQL命令。这多亏了被用于RepositoryBase的SQL调试输出的Kris Vandermotten 方便的DebuggerWriter组件(译者注:外国人就是绅士)!

天下有没有免费的午餐...

当前的加载(Load)操作中,没有任何显著的性能损失,但是当你调用Save or Delete方法时候,幕后用到一点反射(reflection)操作。

对于绝大多数的数据访问层(DAL)需求,在你的应用程序当中,这可能并没有显著的的影响。 但是,如果您正在执行大量的更新/插入/删除操作,特别是大量的包含嵌套的实体,那么您可能需要自己写代码替代Repository的Save / Delete方法。.所有Save / Delete方法都是虚方法(virtual),因此您可以轻易重写(override)他们。

另外请注意, RepositoryBase不支循环依赖(circular dependencies)的递递归save 或者 delete操作。

结论

本文将和包括源代码提供了一个简单,方便和可扩展的方式实现您的多层LINQ数据层CRUD(译者注:增,删,改,查)的方法。.它利用断开模式,并支持保存(saving)和加载(loading)嵌套子实体(child entities).在Save 和Delete(译者注:原文为Load,可能原作者笔误)操作时候有一个小小的性能损失,但在性能至关重要的应用中,您可以重写这些Repositories类的Save和Delete。

对于一切,你安心上路,只需几行代码。 

【编辑推荐】

  1. 使用LINQ to XML来查询XML
  2. SQL连接中加条件查询的LINQ表达式写法
  3. .NET开发工具LINQ框架设计指南
责任编辑:彭凡 来源: 博客园
相关推荐

2009-09-15 10:02:44

Linq to SQL

2009-09-09 16:45:29

Linq调用数据访问服

2009-12-02 10:33:34

LINQ to SQL

2009-09-15 17:07:24

Linq To SQL

2009-09-14 09:46:00

LINQ to SQL

2009-09-10 10:09:46

LINQ to SQL

2009-12-23 09:04:41

LINQ通用分页

2009-09-14 10:12:11

LINQ to SQL

2009-09-14 13:17:51

LINQ to SQLLINQ to SQL

2009-09-18 15:19:19

LINQ to SQL

2009-09-18 14:07:51

LINQ to SQL

2009-09-07 16:13:14

LINQ to SQL

2009-06-08 09:22:07

数据访问优化SQL Server

2009-09-18 14:25:36

LINQ to SQL

2011-03-29 09:15:28

通用数据访问层

2009-09-08 14:45:24

Linq to SQL支持SQL Serve

2009-09-14 19:20:22

LINQ TO SQL

2009-09-09 14:40:43

Linq to sql

2009-09-10 11:29:00

LINQ to SQL

2009-09-08 13:16:01

Linq to SQL
点赞
收藏

51CTO技术栈公众号