程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> LINQ那些事兒(9)-解析Table<T>.Attach引發的異常和解決方法

LINQ那些事兒(9)-解析Table<T>.Attach引發的異常和解決方法

編輯:關於.NET

起因主要是因為看到博客園又有朋友開始討論LINQ2SQL的問題,這次說的是Attach。通過解讀Attach,可以發現LINQ2SQL內部是如何維護和跟蹤對象實例、如何實現延遲加載,並且還可以引發關於延遲加載和N-Tier Application中LINQ2SQL的應用技巧的討論。本文所討論內容適用於.Net Framework 3.5版本的LINQ2SQL,所使用數據庫是Northwnd。

對於對象添加和刪除操作,LINQ2SQL在Table<T>類定義中直接提供了InsertOnSubmit()/DeleteOnSubmit()。而對於對象的更新,由於LINQ2SQL中采取了對象跟蹤的機制(可參考LINQ2SQL對象生命周期管理),所以我們在修改了對象屬性後無需顯式通知DataContext,當調用DataContext.SubmitChanges()時會自動的把我們所做的修改提交到數據庫保存。這種基於上下文的操作是非常方便的,否則在代碼中會出現大量的Update調用,但是也存在限制——只有在同一個 DataContext對象的作用域內,對象所做的修改才會在SubmitChanges()時得到保存。如:

1 using (var context = new Northwnd())
2 {
3     var customer = context.Customers.First();
4     customer.City = "Beijing";
5     context.SubmitChanges();
6 }

而在Web和N-Tier Application開發時,數據查詢和更新同在一個DataContext中往往得不到滿足,所以LINQ2SQL在Table<T>類定義了Attach方法,用於把已與查詢DataContext上下文斷開的對象關聯到Table所屬的DataContext對象,這樣就可以通過新的 DataContext執行對象的更新操作。如:

01 Customer customer = null;
02
03 using (var context1 = new Northwnd())
04 {
05     customer = context1.Customers.First();
06 }
07
08 customer.City = "Beijing";
09
10 using (var context = new Northwnd())
11 {
12     context.Customers.Attach(customer);
13     context.SubmitChanges();
14 }

但是問題來了,這段代碼執行錯誤,拋出以下異常:

System.NotSupportedException: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext.  This is not supported.

這個問題已經不是一個新鮮的問題了,google一下有很多的解決方法,但這看起來很正常的代碼為什麼會拋出異常呢?其實還是和 DataContext的作用域有關的,本文嘗試剖析這個問題,然後還會討論在N-Tier Application中使用LINQ2SQL的一些須知技巧。

都是Association惹的禍?

出錯的地方在System.Data.Linq.Table<T>.Attach(TEntity entity, bool asModified),下列條件只要滿足其中一個,就會造成Attach調用失敗:

1、DataContext為只讀,相當於把DataContext.ObjectTrackingEnabled=false。只讀的DataContext只能進行數據的查詢,其他的操作都會拋出異常。

2、當調用Attach時,DataContext正在執行SubmitChanges()操作。

3、DataContext對象已經被顯式調用Dispose()銷毀。

4、asModified為true,但TEntity類的屬性映射信息(MetaType)中包含 UpdateCheck=WhenChanged或UpdateCheck=Always設置。這是因為Attach的對象在當前DataContext 中並沒有原始值的記錄,DataContext無法根據UpdateCheck的設置生成where字句以避免並發沖突。需要說明的是幾乎不會用到 asModified=true的調用,尤其是在對查詢顯示-用戶修改-提交保存這樣的Web應用場景,本文稍後會討論這樣的場景如何操作。如果堅持要用 asModified=true的調用,那麼可以在TEntity類增加RowVersion屬性的定義,LINQ2SQL引入RowVersion就是為了提供除UpateCheck以外的另一個沖突檢測的方法,由於RowVersion應該在每一次更新操作後都應該修改,所以一般對應 Timestamp類型。

5、嘗試Attach一個已屬於當前DataContext上下文的對象。

6、嘗試Attach的對象包含未載入的Assocation屬性,或是未載入的嵌套Association屬性。

其中原因(6)屬於本文的討論內容,我們來看看Attach函數中的調用

1 ...
2 if (trackedObject == null)
3     {
4         trackedObject = this.context.Services.ChangeTracker.Track(entity, true);
5     }
6 ...

Attach函數中調用StandardChangeTracker.Track(TEntity entity, bool recursive)方法,請注意第二個參數表示遞歸,Attach調用Track(entity, true)會導致entity的所有嵌套Association屬性都會被檢查。代碼就是在StandardChangeTracker.Track中拋出了異常:

1 ...
2 if (trackedObject.HasDeferredLoaders)
3 {
4     throw Error.CannotAttachAddNonNewEntities();
5 }
6 ....

再看看trackedObject.HasDeferredLoaders做了什麼:

01 internal override bool HasDeferredLoaders
02 {
03     get
04     {
05         foreach (MetaAssociation association in this.Type.Associations)
06         {
07             if (this.HasDeferredLoader(association.ThisMember))
08             {
09                 return true;
10             }
11         }
12         foreach (MetaDataMember member in from p in this.Type.PersistentDataMembers
13             where p.IsDeferred && !p.IsAssociation
14             select p)
15         {
16             if (this.HasDeferredLoader(member))
17             {
18                 return true;
19             }
20         }
21         return false;
22     }
23 }

很快就要找到關鍵點了,在看看this.HasDeferredLoader:

01 private bool HasDeferredLoader(MetaDataMember deferredMember)
02 {
03     if (!deferredMember.IsDeferred)
04     {
05         return false;
06     }
07     MetaAccessor storageAccessor = deferredMember.StorageAccessor;
08     if (storageAccessor.HasAssignedValue(this.current) || storageAccessor.HasLoadedValue(this.current))
09     {
10         return false;
11     }
12     IEnumerable boxedValue = (IEnumerable) deferredMember.DeferredSourceAccessor.GetBoxedValue(this.current);
13     return (boxedValue != null);
14 }

答案揭曉:storageAccessor.HasAssignedValue檢測了Association屬性是否被賦值(針對 EntityRef),storageAccessor.HasLoadedValue檢測了Association屬性是否已被加載(針對 EntitySet),如果沒有任何的賦值或加載,並且由GetBoxedValue獲取的延遲源對象(DeferredSource)不為空,則拋出異常。

要解釋Attach為什麼在這種情況下會拋出異常?首先要弄明白延遲源對象,這是LINQ2SQL實現延遲加載的關鍵。在延遲加載的模式 (DataContext.DeferredLoading=true)下,EntitySet和EntityRef屬性只有當被訪問時,才會產生數據庫的查詢。以EntitySet為例,當調用GetEnumerator()時:

1 public IEnumerator<TEntity> GetEnumerator()
2 {
3     this.Load();
4     return new Enumerator<TEntity>((EntitySet<TEntity>) this);
5 }

this.Load中調用了延遲源進行數據的加載:

01 public void Load()
02 {
03     if (this.HasSource)
04     {
05         ItemList<TEntity> entities = this.entities;
06         this.entities = new ItemList<TEntity>();
07         foreach (TEntity local in this.source)
08         {
09             this.entities.Add(local);
10         }
11 ...
12     }
13 }

再進一步就要追溯到 System.Data.Linq.CommonDataServices.GetDeferredSourceFactory(MetaDataMember) 和System.Data.Linq.Mapping.EntitySetValueAccessor。當DataContext對象初始化模型信息時,會調用GetDeferredSourceFactory為指定屬性生成相應的DeferredSourceFactory對象,該工廠對象通過 CreateDeferredSource()生成延遲源對象。在執行查詢操作時,DataContext將會調用每個對象的EntitySet屬性的 SetSource方法,為每一個EntitySet綁定延遲源,由延遲源來調用DataContext實現延遲加載,這樣就實現了EntitySet和 DataContext的解耦,讓POCO類也變智能了。對於EntitySet,當執行延遲加載後,延遲源將被清空,並且相應的已加載標志也將設為 true。

接下來我們驗證一下,為了方便示例我只保留Customer類的Orders作為唯一的Association屬性:(文章最後會給出代碼下載,有興趣可以照著驗證)

01 Customer customer = null;
02
03 using (var context = CreateNorthwnd())
04 {
05     customer = context.Customers.First();
06     // forces to load order association
07     customer.Orders.Count.Dump();
08 }
09  
10 customer.City = "Beijing";
11  
12 using (var context = CreateNorthwnd())
13 {
14     context.Customers.Attach(customer);
15     context.SubmitChanges();
16 }

別急,還是錯的!雖然customer.Orders.Count的調用讓customer.Orders被加載,但Order對象還包含幾個未被加載的Association屬性,你把Order對象的Association屬性定義去掉就對了!

剖析到這裡你明白為什麼當存在Association或嵌套Association未被賦值或加載,且延遲源不為空時會拋出異常了麼?這是因為和需要Attach的對象一樣,延遲源關聯的DataContext對象已經被銷毀了,延遲源無法在加載數據,所以DataContext拒絕關聯這樣的對象。

說了那麼多,是為了讓大家能夠明白為什麼會產生異常,解決的方法很簡單,不需要修改實體的定義,同時也是個人認為LINQ2SQL最佳實踐之一:

01 Customer customer = null;
02
03 using (var context = CreateNorthwnd())
04 {
05     var option = new DataLoadOptions();
06     // disabled the deferred loading
07     context.DeferredLoadingEnabled = false;
08     // specify the association in needed
09     option.LoadWith<Customer>(c => c.Orders);
10     context.LoadOptions = option;
11
12     customer = context.Customers.First();
13 }
14
15 customer.City = "Beijing";
16
17 using (var context = CreateNorthwnd())
18 {
19     context.Customers.Attach(customer);
20     context.Refresh(RefreshMode.KeepCurrentValues, customer);
21     context.SubmitChanges();
22 }

首先我們關閉了DataContext的延遲加載,並且通過DataLoadOption顯式指定了需要加載的關聯數據,這樣的做法不但解決Attach的問題,而且還避免了在N-Tier Application中由於延遲加載所可能導致的異常。

LINQ2SQL最佳實踐

文章最後羅列一些我自己總結的應用LINQ2SQL的心得。

1、建議通過using來使用DataContext對象,這樣當操作完畢立即銷毀DataContext對象。默認狀態下(ObjectTrackingEnabled=true),DataContext將在內存中保存查詢對象的副本,如果長時間保持 DataContext對象,會造成內存不必要的占用。

2、對於僅查詢的操作,設ObjectTrackingEnabled=false,關閉對象跟蹤有助於提高DataContext的查詢性能,這也是所謂的只讀DataContext。再根據所需數據,設置DataLoadOption.AssociateWith()、 DataLoadOption.LoadWith(),可實現高效的查詢。

3、當用LINQ2SQL編寫N-Tier Application時,建議關閉延遲加載,因為這帶來的麻煩遠遠大於好處。注意當ObjectTrackingEnabled=false時,延遲加載是不可用的,相當於DataContext.DeferredLoading=false。

4、Attach(entity, false) + DataContext.Refresh(RefreshMode.KeepCurrentValues, entity) + SubmitChanges(),實現斷開對象的更新,這就避免了DuplicateKey的問題,如上面給出的代碼所示。

麒麟的帖子引發了不少討論,挺有趣的:

1、“方法簽名中不出現linq to sql的實體,方法代碼塊中肯定要出現的。我看人家的開源項目都是在訪問數據庫的時候再將DomainModel轉化為linq to sql的Entity,這樣使用的linq to sql。”

對於N-Tier Application,DomainModel(領域對象)的應用范圍在Presentation Layer和Business Layer之間的層次,而LINQ生成的POCO類屬於DataModel,應用范圍在整個N-Tier。在概念上DomainModel和 DataModel是不同的,但是在大多數的3-tier應用中,DomainModel和DataModel是同一個類——實體類。至於為什麼“人家開源項目”會這樣做,我想大多數原先並不是用LINQ開發,後來移植過來的吧?

2、“先根據傳入product對象的id,查詢出原始的product,然後利用反射自動copy新屬性”

DataContext專門提供了Refresh()函數,可以讀取entity的數據庫值,再通過指定的RefreshMode來刷新 entity的當前值或原始值。在更新entity前,我們首先需要Attach對象,因為通過 DataContext.Refresh(RefreshMode.KeepCurrentValues, entity)可獲得entity的原始值,把判斷entity是否已更改的工作交給DataContext,所以我們只需要調用 Attach(entity)或Attach(entity, false),而不需要調用Attach(entity, true)或Attach(entity, originalEntity),更不需要”copy”了。

3、“樓主的NorthwindDataContext實例化太厲害了,要知道datacontext是個很大的對象,應該避免不停地實例化。最好是一次request只有一個實例,你的問題就迎刃而解了。”

在LINQ2SQL的Design Intent有說過,LINQ2SQL的應用模式是“Unit of work”,即創建-調用-銷毀,目的就是為了在調用完畢後快速釋放DataContext由於保存對象副本和SQL連接所占用的資源,DataContext提供了足夠的機制來保證實例化的消耗在可以接受的范圍。但如果在一次http request裡keep住DataContext對象,不小心反而會造成內存不必要的占用。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved