LINQ to SQL讓人著迷,在.Net應用程序當中,.它提供了一種安全,強大和非常靈活的方式執行數據 訪問,在當前微軟傳道者介紹上看,很容易上手。
不幸的是,當你對LINQ進行仔細研究後,我發現在多層架構中使用LINQ的並不是十分容易。
本文介紹用LINQ to SQL實現數據層的典型的問題點 ,並提供了一個簡單,方便和靈活的方式來克服 它們。
本文附帶的LING 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將用於數據訪問層。
LINQto SQL的問題是-盡管它的許多優點-但是如果要實現數據層並不是很簡單。
請看下面的數據庫模式(database schema):
一旦你要加載和保存LINQ實體到同一個的數據上下文實例(data context instance)(這就是所謂“ 連接模式”),用LINQ實現數據層非常直接。
例如,讓我們從數據庫中獲取實體編號為1的客戶,改變屬性first name為“Homer”後在重新儲存到 數據庫中。在一個多層數據庫應用程序中,在UI或業務層的某個地方的代碼可能看起來就像這樣:
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);
最簡單的方法來實現上面使用到的數據層加載和保存功能是:
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提交更改
在源代碼,它看起來像這樣:
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<Customer>.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實體之間的關系。
例如,假設在業務層或用戶界面層您要做到以下幾點:
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):
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:
1. public class CustomersRepository :
2. //derive from RepositoryBase with the entity name and
3. //data context as generic parameters
4. DeverMind.RepositoryBase
5. {
6. override protected Expression<Func<Customer, bool>> GetIDSelector (int ID)
7. {
8. //ID needs to be the entity's ID column name
9. return (Item) => Item.ID == ID;
10. }
11. }
12. public partial class Customer
13. {
14. public static RepositoryBase CreateRepository()
15. {
16. //create and return an instance of this entity type's repository
17. return new CustomersRepository();
18. }
19. }
您的每一個實體的類型都照這樣做,你就擁有了一個工作在斷開模式下無縫數據層。您繼承 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。
對於一切,你安心上路,只需幾行代碼。
原文地址:http://devermind.com/linq/a-linq-disconnected-mode-abstract-base-class
本文配套源碼:http://www.bianceng.net/dotnet/201212/725.htm