Entity Framework 4 in Action讀書筆記——第六章:理解實體的生命週期

風靈使發表於2018-06-08

(一)

我們先從分析實體的生命週期和它的狀態開始。

實體生命週期

在其生存期期間,一個實體只有一個狀態。在瞭解如何檢索狀態之前,先看看什麼是實體狀態。實體狀態就是宣告為以下值的System.Data.EntityState型別的列舉:

Added——實體標記為added。
Deleted——實體標記為deleted。
Modified——實體已經被修改。
Unchanged——實體還沒有被修改。
Detached——實體不能被追蹤。

這些狀態代表什麼?狀態與什麼相關?實體如何從一個狀態傳遞到另一個狀態?這些狀態影響資料庫嗎?回答這些問題,必須先看看物件生命週期背後的概念。

理解實體狀態

如第三章所述,上下文(context)持有對從資料庫檢索的所有物件的引用。對我們的討論更重要的是,上下文保持實體的狀態並且維護對實體屬性的修改。這一功能稱為change tracking(或object tracking)。

如果在上下文外部建立一個實體,它的狀態為Detached,因為上下文不能追蹤它。

如果將一個實體附加到上下文,它的狀態就變為Unchanged。如果從資料庫中檢索一個實體,然後從上下文中移除,實體的狀態為Detached。如果檢索一個實體,釋放上下文,然後建立一個新的上下文,給它新增一個實體,這個實體的狀態則為Added。這些例子說明,狀態是實體和對它持有引用的上下文間的關係。

讓我們看個例子。假設在OrderIT中有兩個方法,第一個是檢索關於customer的資料,第二個用於更新資料。客戶使用第一個方法檢索資料並在窗體中顯示。在這個方法中,建立了一個上下文來檢索資料,然後銷燬上下文。

使用者修改了一些資料,例如發貨地址(shipping address),然後呼叫第二個方法更新修改的customer資料並儲存它。在網路服務(web service)方法中,建立一個新的上下文並且將實體附加到它。新的上下文並不知道什麼資料已經被修改了,除非它去資料庫中比較。

去資料庫中的代價比較大,所以並不會自動執行。這就是說實體附加到上下文時,它進入Unchanged狀態,因為上下文對修改一無所知。如果實體狀態反映資料庫的實際狀態,它會是Modified,但是事實並非如此。這個例子很簡單,但是它解釋了為什麼狀態是表示實體和上下文間的關係而不是實體和資料庫的關係。

當然,實體的狀態影響它的持久化方式。這並不奇怪,當持久化實體時,在資料庫中使用INSERT, UPDATE, 或DELETE命令儲存那些AddedModified或者Deleted狀態。

實體狀態如何影響資料庫

狀態不僅僅表示上下文中的實體狀態,還表示資料如何持久化到資料庫。每一個狀態,都有一個對應的SQL命令。

Added狀態的實體使用INSERT命令在對映的表中建立一個新行進行持久化。Modified實體在表中已經有了對應的行,所以使用UPDATE命令持久化。Deleted狀態的實體在表中有對應的行,但是它觸發DELETE而不是UPDATE

DetachedUnchanged狀態對資料庫沒有影響:detached實體不能被上下文追蹤,所以不用持久化,而unchanged實體沒有修改的東西需要持久化。

在其生存期期間,實體可以改變其狀態。

實體生命週期的狀態改變

實體的狀態可以由上下文(context)自動設定也可以由開發人員手動設定。儘管從一個狀態到另一個狀態的所有轉換組合都是可能的,但是有一些是沒有意義的。例如,將一個實體從Added狀態轉換到Deleted狀態是沒有意義的,反之亦然。下圖展示了所有的狀態以及一個實體如何從一個狀態傳遞到另一個狀態。

這裡寫圖片描述

圖中描述的非常清晰了,唯一需要說明的就是圖中所示的所有方法都屬於ObjectContext或者ObjectSet<T>類。下面我們詳細的看一下各種狀態。

DETACHED狀態

當一個實體處於Detached狀態,它沒有繫結到上下文(context),所以它的狀態是不能被追蹤的。它可以釋放,修改以及和其他類組合或者其他任何你可能需要的方式使用。因為它沒有上下文追蹤它,它對EF沒有意義。

由於上下文不能追蹤你程式碼中任何物件的建立,因此,Detached是新建立實體的預設狀態。即使你在上下文的using塊中例項化實體也是如此。當追蹤被禁用時,Detached還是從資料庫中檢索的實體的狀態。

UNCHANGED狀態

當實體是Unchanged狀態,它被繫結到上下文,但是它還沒有被修改。預設情況下,從資料庫中檢索的實體是這種狀態。
當實體附加到(使用Attach方法)上下文時,它同樣是Unchanged狀態。上下文不能追蹤它不引用的物件的變化,所以當它們附加到上下文,它們是Unchanged狀態。

ADDED狀態

當實體處於Added狀態,你有很少的選擇。實際上,你只能使用Detach方法將它從上下文中分離。

當然,即使你修改了一些屬性,狀態依然保持為Added,因為轉換到ModifiedUnchanged或者Deleted沒有意義——它是一個新實體並且在資料庫中沒有對應的行。這是處在這些狀態的前提條件。

MODIFIED狀態

當實體是Modified時,這意味著它處在Unchanged狀態,然後改變了一些屬性。

一個實體進入Modified狀態後,它可以轉換到Detached或者Deleted狀態,但是即使手動重置初始值也不能使它回滾到Unchanged狀態(除非從上下文分離再附加到上下文)。它也不能變成Added狀態(除非從上下文分離再新增一個實體到上下文,因為在資料庫中已經存在這個ID的行,當持久化它時,就會得到一個執行時異常)。

DELETED狀態

實體進入Deleted狀態因為它處於Unchanged或者Modified狀態,然後使用了DeleteObject。這是最嚴格的狀態,因為除了轉換成Detached狀態,從這種狀態轉換成任何其他的狀態都是沒有意義的。

下一篇文章學習如何管理實體的狀態。


(二)

管理實體狀態

上下文僅僅自動處理Unchanged狀態到Modified狀態的轉變。其他的狀態轉變必須使用適當的方法顯示處理:

AddObject——在Added狀態時給上下文新增一個實體。
Attach——在Unchanged狀態時附加一個實體到上下文。
ApplyCurrentValuesApplyOriginalValues——改變狀態為Modified,將追蹤的實體與另一個比較。
DeleteObject——標記一個實體為Deleted
AcceptAllChanges——標記所有的實體為Unchanged
ChangeStateChangeObjectState——改變一個實體從一個狀態到另一個狀態沒有任何限制(Detached除外)
Detach——從上下文移除一個實體。

這些方法在ObjectContextObjectSet<T>類中公開,AttachToChangeState除外。ObjectSet<T>方法在內部呼叫上下文的方法,所以兩者沒有什麼區別。

下面我們詳細看一下每一個方法。

AddObject方法

AddObject允許在Added狀態時給上下文新增一個實體。當實體被新增到上下文,為了修改它會被新增到上下文追蹤。當持久化過程被觸發,上下文使用INSERT命令儲存物件為表中的一個新行。在OrderIT例子中,持久化一個order會引起對Order表的INSERT,然而持久化一個shirt會引起對ProductShirt表的INSERT

AddObject上下文方法接受兩個引數,實體集的名稱和實體。

public void AddObject(string entitySetName, object entity)

在這段程式碼中至少有兩個缺點。首先,實體集的名字是以字串傳遞的。如果輸入錯誤,會在執行時得到異常。第二,Object型別的實體引數,意味著你傳遞任意CLR型別,如果這個物件不正確只有在執行時才會得到異常。

在強型別時代,這樣一個API是難以忍受的。為了克服這個糟糕的設計,EF團隊在實體集介面中引進了一個等價的API。它只接受需要新增的物件:

public void AddObject(TEntity entity)

TEntity是由實體集維持的實體型別(記住一個實體集是實現IObjectSet<T>介面型別的例項)。因為強型別,你無法傳遞一個不正確的物件給方法。此外,實體集知道它的名字,沒有必要指定它。

Attach方法

Unchanged狀態時,Attach方法將物件附加到上下文。當實體被附加到上下文,它就由上下文追蹤對標量屬性的修改。

現實世界中的應用程式,一個實體需要被附加有很多情形。Web應用程式和Web服務就是典型的例子。回到前面的例子,在檢索客戶的方法中,建立一個上下文,執行查詢,返回物件給客戶端,然後釋放上下文。在更新的方法中,建立一個新的上下文,然後將customer附加給它。最後,持久化customer

回到Attach方法,Attach上下文方法接收兩個引數:實體集的名稱和實體。這個方法遭受AddObject同樣的限制,所以已經過時了。取而代之,可以使用Attach實體集方法,它只需附加物件,看下面的清單:

var c = new Customer { CompanyId = 1 };
...
ctx.Companies.Attach(c);

你附加一個物件給上下文,因為你想它的資料在資料庫中被更新。當然,物件必須在資料庫中有對應的行,這個對應由主鍵屬性和列標識的。

當附加一個物件給上下文時,主鍵列必須設定,否則會在執行時得到一個InvalidOperationException。而且,在永續性階段,如果UPDATE命令不能作用於任何行,上下文會丟擲異常。

因為附加的實體要離開Unchanged狀態,你必須找到一種方式標記它為Modified在資料庫裡持久化它。下一個要討論的方法完成這個工作。

ApplyCurrentValuesApplyOriginalValues方法

在我們的客戶web服務例子中,當已經附加了物件,就需要持久化它。問題是,它被附加後還是Unchanged狀態,所以需要找到一種方式改變它為Modified狀態。最簡單的方式是在資料庫中查詢最新的資料並將它與輸入的實體比較。

你知道物件上下文保持有一個對每個實體的引用,這個實體或者是從資料中檢索出的或者是通過AddObject或者Attach方法附加的。我們沒有提到的是,當實體被繫結到上下文時,標量屬性的原值(original values)和當前值(current values)儲存在記憶體中。

ApplyOriginalValues方法將實體作為輸入(來自資料庫)。然後這個方法從上下文的記憶體中檢索一個相同型別以及具有相同鍵的物件作為輸入實體。最後,該方法複製輸入實體的標量屬性的值給上下文實體的標量屬性的原值。目前,儲存在上下文中標量屬性的原值包含來自資料庫的資料,然而儲存在上下文中的標量屬性的當前值包含來自web服務的實體的值。如果原值不同於當前值,實體就被設定為Modified狀態;否則它仍然是Unchanged

也可以按照相反的路徑。查詢資料庫並且從web服務的實體應用修改代替附加實體並且查詢資料庫。這是ApplyCurrentValues方法所做的事情。它將一個實體作為輸入(來自web服務)。然後該方法在上下文記憶體中檢索一個相同型別以及具有相同鍵的物件作為輸入實體。最後,該方法複製輸入實體的標量屬性值到上下文實體的標量屬性的當前值。目前,儲存在上下文的當前值包含來自web服務實體的資料,並且原值是來自資料庫的值。如果它們不同,實體就被設定為Modified狀態,否則,它仍然是Unchanged

當持久化被觸發,如果實體是Modified狀態,它就用UPDATE命令持久化。

如我們前邊討論的方法,ApplyOriginalValuesApplyCurrentValues方法屬於ObjectContextObjectSet<T>類,我們建議使用後者公開的方法,如下:

var entityFromDb = GetEntityFromDb(entityFromService.CompanyId);
ctx.Companies.Attach(entityFromService);
ctx.Companies.ApplyOriginalValues(entityFromDb);
ctx.Companies.First(c => c.CompanyId == entityFromService.CompanyId);
ctx.Companies.ApplyCurrentValues(entityFromService);

這裡你必須意識到有一點點的陷阱。兩個方法僅僅關心輸入實體的標量和複雜屬性。如果一個關聯實體的標量屬性改變,或者在關聯集合中一個新行被新增,移除或者修改,它不會被檢測到。
DeleteObject方法

DeleteObject方法標記一個實體為Deleted。唯一需要注意的是你必須牢記傳遞到該方法的實體必須附加到上下文。該物件必須來自查詢或者已經使用Attach方法附加到了上下文。如果在上下文中沒有找到該物件,就會丟擲一個InvalidOperationException異常,附帶一條資訊:The object cannot be deleted because it was not found in the ObjectStateManager。

下面的清單顯示了使用由ObjectSet<T>類公開的DeleteObject方法。

var c = ctx.Companies.OfType<Customer>().Where(w => w.CompanyId == 1); 
ctx.Companies.DeleteObject(c);

var c = new Customer { ... }; 
ctx.Companies.Attach(c); 
ctx.Companies.DeleteObject(c);

DeleteObject被呼叫,實體沒有從上下文刪除;它被標記為deleted。當持久化被觸發,實體從上下文移除並且執行DELETE命令從資料庫刪除它。

AcceptAllChanges方法

AcceptAllChanges方法接受所有AddedModified狀態的實體並標記它們為Unchanged。然後分離所有Deleted狀態的實體,最後更新ObjectStateManager條目。

AcceptAllChangesObjectContext公開,在ObjectSet<T>類中沒有對用的方法。這就是為什麼需要使用下面的程式碼:

ctx.AcceptAllChanges();

ChangeStateChangeObjectState方法

ChangeStateChangeObjectState方法是靈活的方法。它們允許改變一個實體的狀態到任何其他可能的狀態(Detached除外)。當使用一個實體時,這些方法非常有用。不過當處理複雜的物件圖時,它們的重要性也增加,這在本章後面討論。

ChangeStateObjectStateEntry類公開,而ChangeObjectStateObjectStateManager類公開。ChangeState只需要新的狀態,因為ObjectStateEntry的例項已經指的是一個實體。ChangeObjectState接受實體和新的狀態作為引數。兩個方法如下面的清單所示:

var osm = ctx.ObjectStateManager;
osm.ChangeObjectState(entity, EntityState.Unchanged);
osm.GetObjectStateEntry(entity).ChangeState(EntityState.Unchanged);

這些方法並不總是物理的更改實體狀態;有時使用先前的方法。例如,改變一個實體的狀態為Unchanged意味著呼叫ObjectStateEntry類的AcceptChanges方法。相反,改變一個實體的狀態從UnchangedAdded意味著改變狀態。
有時並不需要實體被持久化或者由上下文追蹤修改。如果那樣,可以將實體從上下文移除。

Detach方法

Detach方法從上下文追蹤的實體的列表移除實體。不管實體處於什麼狀態,它都會變成Detached,但是由分離的(detached)實體引用的實體不能分離(detached)。

呼叫該方法非常簡單,如下面的清單,因為它只接受必要分離的實體。

ctx.Companies.Detach(c);

成功分離的前提條件是實體已經被附加到了上下文。如果沒有,就會得到一個InvalidOperationException異常,附帶一條資訊The object cannot be detached because it is not attached to the Object-StateManager。


(三)

objectstatemanager更改跟蹤管理

ObjectStateManager元件(從現在開始稱之為 state manager)負責與上下中物件追蹤有關的一切:

  1. 當新增,附加到上下文或者從上下文中刪除一個實體,實際上是對state manager做的這些。
  2. 當我們說上下文保留從資料庫中讀取的所有實體集合在記憶體中時,其實是state manager儲存這些資料。
  3. 當上下文執行一個身份地圖(identity-map)檢查,其實是state manager執行的檢查。
  4. 當我們說上下文跟蹤實體間關係式,其實是state manager在跟蹤。

跟蹤實體改變僅僅是state manager的任務之一。它還提供檢索實體狀態和操作它的API。

state manager不是直接訪問的。因為它是上下文的內部元件,它以ObjectContext類的屬性公開,叫ObjectStateManager。下面的程式碼訪問state manager的程式碼:

var osm = ctx.ObjectStateManager;

上下文負責state manager的生命週期,它處理它的初始化和釋放。

現在已經知道了state manager的目的,讓我們深入看看它如何完成任務。

ObjectStateEntry

當查詢state manager來檢索由上下文跟蹤的實體,它由ObjectStateEntry(從現在開始稱為entry)物件應答。它公開了兩種型別的成員:屬性和方法。

成員 描述
Entity屬性 state manager跟蹤的實體
EntityKey 屬性 實體的Key
EntitySet屬性 實體屬於的實體集
EntityState屬性 實體的狀態
OriginalValues屬性 當每個實體附加時的值
CurrentValues屬性 每個實體的當前值
GetModifiedProperties方法 從實體被跟蹤修改的屬性
IsRelationship屬性 指定entry是否包含有關實體或關係的資料

最重要的成員是EntityState,OriginalValuesCurrentValues。注意OriginalValueshCurrentValuesDbDataRecord型別的。

ObjectStateEntry是抽象類,作為EntityEntryRelationshipEntry的基類。它們都是內部類,所以不能直接操作它們。根據它們的名字,EntityEntry包含關於實體的資料,RelationshipEntry包含關於實體間關係的資訊。

EntityKey屬性很重要,因為它表示state manager內實體的鍵(key)。

理解state managerkey是如何標識物件的?

EntityKey屬性是state manager用來確保即有一個給定型別和ID的實體被跟蹤。身份地圖(identity-map)檢查是檢查實體的EntityKey屬性而不是實體的鍵屬性。EntityKey包含兩個重要的屬性:實體集和組合成實體主鍵的屬性的值。

當新增一個物件到上下文,就使用臨時實體鍵新增物件到state manager,因為EF知道它必須持久化物件為一個新行。這個臨時鍵沒有經過身份地圖檢查評估,所以如果再新增另一個相同型別和ID的物件,它會使用另一個臨時鍵新增到state manager。當持久化時,就會執行兩個INSERT命令。

如果行的ID是由資料庫自動生成的,持久化沒有問題,如果使用自然鍵,持久化就會丟擲一個duplicate-key的異常,因為第二個INSERT命令使用相同的ID,在資料庫中導致主鍵衝突。

當附加實體時,state manager自動建立一個EntityKey物件並儲存在entry(ObjectStateEntry)中。這個EntityKey物件不是臨時的,它由身份地圖檢查(identity-map check)使用。

ObjectStateEntry不僅包含資料,它還合併行為。它允許改變實體的狀態以及重寫原值和當前值。得到ObjectStateEntry例項的唯一方式是查詢state manager

檢索entry

已經清楚的瞭解了新增,附加和刪除實體,為什麼還需要為了實體狀態查詢上下文?有兩種情況非常有用:第一,EF本身需要查詢物件狀態;第二,你可能需要在一些通用日誌記錄或其他場景中報告實體狀態。

假設你想記錄每一個由應用程式觸發的持久化操作。一種方式可能是建立一個執行附加、新增或者刪除並且新增一個entry到記錄儲存的擴充套件方法。這種方法的實現可能某些原因需要中止持久化過程並且結束還沒有發生的記錄操作。
另一種方法是訂閱SavingChanges事件,它在持久化過程開始前(SaveChagnes)觸發,在Added,ModifiedDeleted狀態中檢索實體並且在日誌中寫入entry。這個解決方法如下面的清單所示:

第一步是掛鉤SavingChagnes時間。然後,在處理程式中,使用ObjectStateManager類的GetObjectEntries方法檢索特定狀態的所有entry。它接受一個EntityState引數要查詢的狀態,返回一個特定狀態所有entry的集合。如果不同狀態的entry,可以使用標誌語法組合它們。做種,呼叫logger方法寫入entry

通常,只需要檢索單個entryGetObjectStateEntries在這種情況下不可用。你需要另一個方法,允許傳遞一個實體,得到相對應的狀態管理器(state-manager)的entrystate manager有這樣一個方法。

檢索單個entry

檢索單個實體的entry,可以使用GetObjectStateEntry方法,傳遞實體作為引數,如下所示:

var entry = osm.GetObjectStateEntry(entity);

輸入實體必須有key屬性集,因為當state manager嘗試檢索entry,它使用它們建立一個EntityKey執行查詢。如果entry不包含這個EntityKey,方法就會丟擲一個InvalidOperationException異常,附帶一條資訊:The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type ‘type’。

為了避免這個異常,可以使用TryGetObjectStateEntry。它和GetObjectStateEntry執行相同的任務;但是遵循了.NET Framework的設計指南,這個方法接收一個實體和一個表示entry找到的輸出引數,它返回一個布林值指定entry是否找到。如果返回false,輸出引數為null。看下面的清單:

ObjectStateEntry entry;
var found = osm.TryGetObjectStateEntry(c, out entry);

使用ObjectStateEntry類,可以使用ChangeState修改實體的狀態,如前面所見。但那不是唯一的選擇。下面討論其他允許修改實體狀態的方法。

由entry修改實體狀態

當有了entry,就可以修改相關實體的狀態,因為上下文方法在內部呼叫ObjectStateEntry類的方法。這些方法如下表所示:

方法 描述
Delete 標記實體為deleted。當移動到Deletted狀態時這個方法也由DeleteObjectChangeState呼叫。
SetModified 標記實體以及它的所有屬性為Modified。當移動到Modified時這個方法在內部由ChangeState呼叫。
SetModifiedProperty 標記一個屬性為Modified,因此也標記實體。
AcceptChanges 改變實體的狀態為Unchanged並使用當前值重寫entry的原值。
ChangeState 改變實體的狀態到輸入值。

這些方法使用很簡單,因為它們中的大多數都沒有引數。只有SetModifiedProperty接收屬性的名稱和ChangeState接收實體新的狀態。

前面提到由state manager自動執行的唯一狀態改變是從UnchangedModified,但它並不總是這樣。下面,深入物件跟蹤機制。

理解物件跟蹤

從技術上來說,state manager不能監視實體內屬性的修改;當修改發生時,實體通知state manager。這種通知機制並不總是起作用——它依賴於你如何初始化實體。可以建立下面型別的實體:

  1. 沒有被代理包裝POCO實體(普通實體)
  2. 由代理包裝的實體(代理實體)
    包裝的實體是一個類,它通過代理啟用擴充套件性。當類的繼承不是封閉的和它的屬性是virtual時,它就包裝的。尤其是如果所有的標量屬性都是virtual,包裝類啟用更改追蹤。包裝(或代理)實體是已經包裝到虛擬代理(virtual proxy)中的實體的例項。

state manager 並不關心類包裝與否。重要的是實體是作為代理或者POCO類被例項化。下面我們看一些例子說明它們的區別。

實體的更改追蹤沒有包裝在代理中

實體可能從web服務,web頁面的ASP.NET ViewState的反序列化,上下文代理建立禁用的查詢,建構函式初始化獲得。這些物件沒有被代理包裝,因為只有啟用代理建立的上下文可以建立包裝的實體。此外,一個實體可能不是包裝的,所以即使它來自上下文,也可能不是代理的。

如第5章中所見,實體的屬性setter器不知道state manager,那麼state manager是如何知道屬性什麼時候被修改的呢?你也許會驚訝於它不能。

我們看個例子。假設你需要修改一個customer。你查詢資料庫檢索customer並修改屬性,如name,然後持久化它。因為state manager不知道你已經修改了屬性,實體的狀態仍然保持在Unchanged,如下所示:

var customer = ctx.Customers.First();
var entry = osm.GetObjectStateEntry(customer);
customer.Name = "NewCustomer";                  //State Unchanged
ctx.SaveChanges();

SaveChanges方法被呼叫,即使狀態是Unchanged,修改也被持久化到資料庫。這是怎麼做到的呢?為什麼state manager不知道的情況下修改被持久化了呢?

神奇之處在於ObjectStateManager類的DetectChanges方法,它在內部由SaveChanges方法呼叫。這個方法遍歷所有的狀態管理器(state-managerentry,並且比較每一個的原值和儲存在實體中的值。當它發現屬性被修改——在本例中,是customername——它標記屬性為Modified,進而標記實體為Modified,並且更新entry的當前值。當DetectChanges完成它的任務,state manager中的實體和它們的entry完美的同步,SaveChanges可以繼續持久化了。

由於state manager並不會與實體自動同步,無論合適使用它的API,你必須呼叫DetectChanges方法避免檢索過期資料,如前面的清單。看下面的清單:

var entry = osm.GetObjectStateEntry(customer);
customer.Name = "NewCustomer";                      // State Unchanged
ctx.DetectChanges();                               //State Modified

DetectChanges並不是沒有問題。它遍歷所有的實體並檢查它們所有的屬性。如果許多實體被跟蹤,遍歷可能非常浪費。使用它,但不濫用。

更改追蹤包裝在代理中

當實體被包裝在代理中,它下面有更多的神奇。代理實體使自動更改跟蹤成為可能,意味著當屬性變化時它能及時通知state manager。這是因為代理重寫屬性setter器,注入程式碼通知state manager屬性發生了改變。下圖包含了一個代理內部簡單的程式碼版本。

這裡寫圖片描述

這個功能很棒,不用費勁就實現了state manager和實體的自動同步。看下面的程式碼:

var entry = osm.GetObjectStateEntry(customer);       //State Unchanged
customer.Name = "NewCustomer";                       //State Modified

在第三章中已經瞭解到代理啟用延遲載入。在本章,已經瞭解到代理還可以啟用自動更改追蹤。在EF1.0,這些功能需要一大堆程式碼,現在好了,實現它們只需一點點程式碼。

上下文不僅能追蹤單個實體,它還可以追蹤實體的關係。你可能以為這是使用關聯物件的主鍵屬性或外來鍵實現的;有時它是這種方式,有時候又不是。

理解關係跟蹤

當實體附加到上下文,一個新的entry被新增到state manager。然後,上下文掃描導航屬性查詢關聯實體。不為null的實體自動附加。當新增實體時也是這樣。

當關聯實體被附加,如果關係是通過獨立關聯,一個新的RelationshipEntry被新增到state manager,包含關聯實體的相關資訊。例如,如果你附加一個order,它有一個對customer和多個order detail的引用,state manager包含order,它的detail,它的customer實體和它們的關聯entry。下圖顯示了附加過程後的state manager

如果order使用查詢載入,會有一點點不同。state managerordercustomer和它們的關聯各建立一個entry。(忽略order detail,因為集合關聯被忽略)。即使不檢索帶有order的customer也是一樣。customer entry值包含主鍵(在Order表中為CustomerId),然而關係指向兩個實體,所以state manager有它需要的一切。

關係可以處於Added或者Deleted狀態,但是不能處於Modified狀態。通常,不需要修改關係狀態,因為它由state manager處理。罕見情況下,需要修改關係狀態時,可以使用ObjectStateManager類的ChangeRelationshipState方法或者ObjectStateEntry類的ChangeState方法。當然,如果你嘗試修改關係狀態為Modified,會得到執行時異常。

如果使用了外來鍵關聯,就不會建立關係entry,因為不需要關聯實體,僅僅是外來鍵屬性。結果是與以前一樣附加order後,state manager看起來如下圖:

image

當實體是從資料庫中檢索的,在state manager中實體entry和關係entry都不會被建立。此外,改變關係是沒有價值的,因為你僅僅需要改變外來鍵屬性。如你所見,外來鍵關聯是事情變得簡單,減少了state manager的工作。

現在已經知道了state manager是如何跟蹤實體和關係的,讓我們研究幾個注意事項。

只有實體被上下文跟蹤,改變才會被跟蹤

當實體在上下文範圍之外,對它們的改變不能跟蹤。如果建立一個order,新增一個detail,或者改變它關聯的customer,然後附加order到上下文,上下文永遠都不會知道發生了什麼。order和關係entry附加時處於Unchanged狀態。

state manager不支援部分載入影象

當附加一個實體,上下文掃描所有的導航屬性,同時附加相關的物件。(新增一個物件到上下文也是如此)如果它們被附加,所有的實體都處於Unchanged狀態,如果它們被新增,則處於Added狀態。

如果上下文已經跟蹤了與關係圖中的實體具有一樣的型別和鍵值的實體,則會引發一個InvalidOperationException異常,因為它不能儲存有同鍵值同型別的兩個物件。

當新增一個關係圖,沒有異常的風險,因為實體鍵合物件的關聯是臨時的。如果實體以後標記為Unchanged會引發問題。在這種情況下,EntityKey再生並且變得永久,如果已經存在了相同鍵的實體,會丟擲一個InvalidOperationException異常,附帶資訊:AcceptChanges cannot continue because the object’s key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges

在單引用(single-reference)屬性中如何更改關係

假設必須更改關聯有ordercustomer。在兩種情況下可以找到自己:

  1. Customer已經附加到上下文。如果外來鍵關聯起作用,屬性是同步的,Order物件變成Modified。如果使用獨立關聯,實體間只有RelationshipEntry被建立。
  2. Customer沒有附加到上下文。CustomerAdded狀態被新增到上下文(記住它不支援部分關係圖)。如果關聯使用外來鍵關聯保持,屬性是同步的並且Order物件變成Modified。如果使用獨立關聯,實體間只有RelationshipEntry被建立。

假設有一個沒有客戶的訂單。如果使用外來鍵關聯,設定外來鍵屬性為null使ordercustomer的關聯消失。如果使用獨立關聯,設定Customer屬性為null,也導致同樣的結果(RelationshipEntry變成Deleted)。

記住只有customerorder之間的關聯被移除。沒有物件被刪除。

在集合屬性中如何更改關係

集合屬性呼叫Remove方法導致對master的引用從detail上移除。例如,當從一個order上移除一個detail,它的Order屬性被設定為null,它的狀態被設定為Modified。因為外來鍵屬性(OrderId)不為空,在持久化時,會出現InvalidOperationException異常,附帶有一條資訊:The operation failed: The relationship could not be changed because one or more of the foreign-key properties is nonnullable。當更改關係時,關聯的外來鍵屬性被設定為null值。如果外來鍵不支援null值,一個新的關係必須定義,外來鍵屬性必須分配到另一個不為空的值,或者不關聯的物件必須刪除。

雖然這看起來像加密了一樣,其實很清晰。detailOrderId屬性不能為null,因為你不能有獨立的order detaildetail必須分配到一個order。如果支援獨立的detail,在OrderDetail表的OrderId列則是可空的。同樣,OrderDetail類的OrderID屬性將使可空的。如果這樣,持久化不會丟擲任何異常,所以order會被持久化,order detail變成獨立的(當然,獨立的detail是沒有意義的)。

如果使用獨立關聯,會得到不同的訊息:A relationship from the ‘OrderOrderDetail’AssociationSet is in the ‘Deleted’ state. Given multiplicity constraints, a corresponding ‘OrderDetail’ must also in the ‘Deleted’ state。意思是,state manager中的RelationshipEntry被刪除了,但是實體是Modified,這是不允許的,因為獨立的order detail是不允許的,detail也必須被刪除。

這個問題的解決方案是呼叫上下文的DeleteObject方法代替集合屬性的Remove方法。

你可能會對EF為什麼不自動刪除實體而是簡單的移除引用產生疑問。答案是在其他情況下這是不正確的行為。想想supplierproduct的多對多關係。如果是那樣,如果移除由supplier賣的product,你不必刪除它。你只需刪除Product-Supplier表的引用。由於這些不同的行為,EF團隊謹慎地決定讓你顯示選擇怎麼做。

當新增一個實體到集合屬性,你可以在兩種不同的情況下找到自己,依賴於實體是否附加到上下文:

  1. detail被附加到上下文。如果外來鍵關聯起作用,屬性必須與order的ID同步。如果使用獨立關聯,只需在實體間建立RelationishipEntry
  2. detail沒有附加到上下文。CustomerAdded狀態(記住,上下文不支援部分關係圖)被新增到上下文。如果關聯使用外來鍵關聯保持,屬性必須與order的ID保持同步。如果使用獨立關聯,實體間也建立RelationshipEntry

有很多的規則,提前瞭解它們可以使操作關係圖簡單點。

更改跟蹤和MergeOption

MergeOptionObjectSet<T>類的一個屬性。它是System.Data.Objects.MergeOption型別的列舉,包含下列值:

1.AppendOnly
2.NoTracking
3.OverwriteChanges
4.PreserveChanges

在物件的具體化期間,當使用AppendOnly(預設設定),state manager檢查是否已經存在了相同keyentry。如果是,返回與entry相關的實體和放棄來自資料庫的資料。如果沒有,實體被具體化並附加到上下文。這種情況下,state manager使用具體化的原值和當前值建立entry。最後,返回具體化的實體。

當使用NoTracking是,上下文不執行身份地圖檢查,所以來自資料庫的資料總是具體化和返回,即使在state manager中已經有了相對應的實體。當NoTracking啟用時,返回的實體處於Detached狀態,所以上下文不跟蹤它們。

當使用OverwriteChanges使用時,如果身份地圖檢查在state manager沒有找到entry,就具體化實體,附加到上下文並返回。如果entry找到了,相關的實體狀態設定為Unchanged,當前值和原值使用來自資料庫中的值更新。

當使用PreserveChanges是,如果在state manager中身份地圖檢查沒有找到entry,就具體化實體,附加到上下文並返回。如果找到了,有以下發生的可能性:

如果實體的狀態是Uchangedentry中的當前值和原值由資料庫的值重寫。實體的狀態仍保持為Unchanged
如果實體的狀態是Modified,修改的屬性的當前值不能被資料庫的值重寫。沒有修改的屬性的原值由資料庫的值重寫。
如果沒有修改的屬性的當前值不同於來自資料庫的值,屬性標記為Modified。從1.0版本這是一個重大的改變,因為在那個版本的屬性不標記為Modified。如果需要恢復1.0的行為,設定UseLegacyPreserveChangesBehavior屬性為true即可,如下:

ctx.ContextOptions.UseLegacyPreserveChangesBehavior = true;

現在已經瞭解了MergeOption行為。任何應用程式中,它都是重要的一方面,它也經常會被濫用或者被輕視。

相關文章