LINQ那些事(9)-解析Table.Attach引發的異常和解決方案

來源:互聯網
上載者:User

起因主要是因為看到部落格園又有朋友開始討論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()時得到儲存。如:

            using (var context = new Northwnd())            {                var customer = context.Customers.First();                customer.City = "Beijing";                context.SubmitChanges();            }

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

            Customer customer = null;            using (var context1 = new Northwnd())            {                customer = context1.Customers.First();            }            customer.City = "Beijing";            using (var context = new Northwnd())            {                context.Customers.Attach(customer);                context.SubmitChanges();            }

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

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函數中的調用

...if (trackedObject == null)    {        trackedObject = this.context.Services.ChangeTracker.Track(entity, true);    }...

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

...if (trackedObject.HasDeferredLoaders){throw Error.CannotAttachAddNonNewEntities();}....

 

再看看trackedObject.HasDeferredLoaders做了什麼:

internal override bool HasDeferredLoaders{    get    {        foreach (MetaAssociation association in this.Type.Associations)        {            if (this.HasDeferredLoader(association.ThisMember))            {                return true;            }        }        foreach (MetaDataMember member in from p in this.Type.PersistentDataMembers            where p.IsDeferred && !p.IsAssociation            select p)        {            if (this.HasDeferredLoader(member))            {                return true;            }        }        return false;    }}

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

private bool HasDeferredLoader(MetaDataMember deferredMember){    if (!deferredMember.IsDeferred)    {        return false;    }    MetaAccessor storageAccessor = deferredMember.StorageAccessor;    if (storageAccessor.HasAssignedValue(this.current) || storageAccessor.HasLoadedValue(this.current))    {        return false;    }    IEnumerable boxedValue = (IEnumerable) deferredMember.DeferredSourceAccessor.GetBoxedValue(this.current);    return (boxedValue != null);} 

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

 

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

public IEnumerator<TEntity> GetEnumerator(){    this.Load();    return new Enumerator<TEntity>((EntitySet<TEntity>) this);}

this.Load中調用了延遲源進行資料的載入:

public void Load(){    if (this.HasSource)    {        ItemList<TEntity> entities = this.entities;        this.entities = new ItemList<TEntity>();        foreach (TEntity local in this.source)        {            this.entities.Add(local);        }...    }}

再進一步就要追溯到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屬性:(文章最後會給出代碼下載,有興趣可以照著驗證)

            Customer customer = null;                        using (var context = CreateNorthwnd())            {                customer = context.Customers.First();                // forces to load order association                customer.Orders.Count.Dump();            }            customer.City = "Beijing";            using (var context = CreateNorthwnd())            {                context.Customers.Attach(customer);                context.SubmitChanges();            }

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

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

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

            Customer customer = null;            using (var context = CreateNorthwnd())            {                var option = new DataLoadOptions();                // disabled the deferred loading                context.DeferredLoadingEnabled = false;                // specify the association in needed                option.LoadWith<Customer>(c => c.Orders);                context.LoadOptions = option;                customer = context.Customers.First();            }            customer.City = "Beijing";            using (var context = CreateNorthwnd())            {                context.Customers.Attach(customer);                context.Refresh(RefreshMode.KeepCurrentValues, customer);                context.SubmitChanges();            }

首先我們關閉了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的問題,如上面給出的代碼所示。

下載

Demo Code: http://files.cnblogs.com/chwkai/LinqAttach.rar

連結

LINQ那些事(總)

LINQ那些事(6) - 對象生命週期管理

相關討論

麒麟的文章引發了不少討論,挺有趣的:

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對象,不小心反而會造成記憶體不必要的佔用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.