Entity Framework 4 in Action讀書筆記——第六章:理解實體的生命週期
(一)
我們先從分析實體的生命週期和它的狀態開始。
實體生命週期
在其生存期期間,一個實體只有一個狀態。在瞭解如何檢索狀態之前,先看看什麼是實體狀態。實體狀態就是宣告為以下值的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命令儲存那些Added,Modified或者Deleted狀態。
實體狀態如何影響資料庫
狀態不僅僅表示上下文中的實體狀態,還表示資料如何持久化到資料庫。每一個狀態,都有一個對應的SQL命令。
Added狀態的實體使用INSERT命令在對映的表中建立一個新行進行持久化。Modified實體在表中已經有了對應的行,所以使用UPDATE命令持久化。Deleted狀態的實體在表中有對應的行,但是它觸發DELETE而不是UPDATE。
Detached和Unchanged狀態對資料庫沒有影響:detached實體不能被上下文追蹤,所以不用持久化,而unchanged實體沒有修改的東西需要持久化。
在其生存期期間,實體可以改變其狀態。
實體生命週期的狀態改變
實體的狀態可以由上下文(context)自動設定也可以由開發人員手動設定。儘管從一個狀態到另一個狀態的所有轉換組合都是可能的,但是有一些是沒有意義的。例如,將一個實體從Added
狀態轉換到Deleted
狀態是沒有意義的,反之亦然。下圖展示了所有的狀態以及一個實體如何從一個狀態傳遞到另一個狀態。
圖中描述的非常清晰了,唯一需要說明的就是圖中所示的所有方法都屬於ObjectContext
或者ObjectSet<T>
類。下面我們詳細的看一下各種狀態。
DETACHED狀態
當一個實體處於Detached
狀態,它沒有繫結到上下文(context),所以它的狀態是不能被追蹤的。它可以釋放,修改以及和其他類組合或者其他任何你可能需要的方式使用。因為它沒有上下文追蹤它,它對EF沒有意義。
由於上下文不能追蹤你程式碼中任何物件的建立,因此,Detached
是新建立實體的預設狀態。即使你在上下文的using
塊中例項化實體也是如此。當追蹤被禁用時,Detached
還是從資料庫中檢索的實體的狀態。
UNCHANGED狀態
當實體是Unchanged
狀態,它被繫結到上下文,但是它還沒有被修改。預設情況下,從資料庫中檢索的實體是這種狀態。
當實體附加到(使用Attach
方法)上下文時,它同樣是Unchanged
狀態。上下文不能追蹤它不引用的物件的變化,所以當它們附加到上下文,它們是Unchanged
狀態。
ADDED狀態
當實體處於Added
狀態,你有很少的選擇。實際上,你只能使用Detach
方法將它從上下文中分離。
當然,即使你修改了一些屬性,狀態依然保持為Added
,因為轉換到Modified
,Unchanged
或者Deleted
沒有意義——它是一個新實體並且在資料庫中沒有對應的行。這是處在這些狀態的前提條件。
MODIFIED狀態
當實體是Modified
時,這意味著它處在Unchanged
狀態,然後改變了一些屬性。
一個實體進入Modified
狀態後,它可以轉換到Detached
或者Deleted
狀態,但是即使手動重置初始值也不能使它回滾到Unchanged
狀態(除非從上下文分離再附加到上下文)。它也不能變成Added
狀態(除非從上下文分離再新增一個實體到上下文,因為在資料庫中已經存在這個ID的行,當持久化它時,就會得到一個執行時異常)。
DELETED狀態
實體進入Deleted
狀態因為它處於Unchanged
或者Modified
狀態,然後使用了DeleteObject
。這是最嚴格的狀態,因為除了轉換成Detached
狀態,從這種狀態轉換成任何其他的狀態都是沒有意義的。
下一篇文章學習如何管理實體的狀態。
(二)
管理實體狀態
上下文僅僅自動處理Unchanged
狀態到Modified
狀態的轉變。其他的狀態轉變必須使用適當的方法顯示處理:
AddObject
——在Added
狀態時給上下文新增一個實體。
Attach
——在Unchanged
狀態時附加一個實體到上下文。
ApplyCurrentValues
和ApplyOriginalValues
——改變狀態為Modified
,將追蹤的實體與另一個比較。
DeleteObject
——標記一個實體為Deleted
。
AcceptAllChanges
——標記所有的實體為Unchanged
。
ChangeState
和ChangeObjectState
——改變一個實體從一個狀態到另一個狀態沒有任何限制(Detached
除外)
Detach
——從上下文移除一個實體。
這些方法在ObjectContext
和ObjectSet<T>
類中公開,AttachTo
和ChangeState
除外。ObjectSet<T>
方法在內部呼叫上下文的方法,所以兩者沒有什麼區別。
下面我們詳細看一下每一個方法。
AddObject
方法
AddObject
允許在Added
狀態時給上下文新增一個實體。當實體被新增到上下文,為了修改它會被新增到上下文追蹤。當持久化過程被觸發,上下文使用INSERT
命令儲存物件為表中的一個新行。在OrderIT
例子中,持久化一個order
會引起對Order
表的INSERT
,然而持久化一個shirt
會引起對Product
和Shirt
表的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
在資料庫裡持久化它。下一個要討論的方法完成這個工作。
ApplyCurrentValues
和ApplyOriginalValues
方法
在我們的客戶web服務例子中,當已經附加了物件,就需要持久化它。問題是,它被附加後還是Unchanged
狀態,所以需要找到一種方式改變它為Modified
狀態。最簡單的方式是在資料庫中查詢最新的資料並將它與輸入的實體比較。
你知道物件上下文保持有一個對每個實體的引用,這個實體或者是從資料中檢索出的或者是通過AddObject
或者Attach
方法附加的。我們沒有提到的是,當實體被繫結到上下文時,標量屬性的原值(original values)和當前值(current values)儲存在記憶體中。
ApplyOriginalValues
方法將實體作為輸入(來自資料庫)。然後這個方法從上下文的記憶體中檢索一個相同型別以及具有相同鍵的物件作為輸入實體。最後,該方法複製輸入實體的標量屬性的值給上下文實體的標量屬性的原值。目前,儲存在上下文中標量屬性的原值包含來自資料庫的資料,然而儲存在上下文中的標量屬性的當前值包含來自web服務的實體的值。如果原值不同於當前值,實體就被設定為Modified
狀態;否則它仍然是Unchanged
。
也可以按照相反的路徑。查詢資料庫並且從web服務的實體應用修改代替附加實體並且查詢資料庫。這是ApplyCurrentValues
方法所做的事情。它將一個實體作為輸入(來自web服務)。然後該方法在上下文記憶體中檢索一個相同型別以及具有相同鍵的物件作為輸入實體。最後,該方法複製輸入實體的標量屬性值到上下文實體的標量屬性的當前值。目前,儲存在上下文的當前值包含來自web服務實體的資料,並且原值是來自資料庫的值。如果它們不同,實體就被設定為Modified
狀態,否則,它仍然是Unchanged
。
當持久化被觸發,如果實體是Modified
狀態,它就用UPDATE
命令持久化。
如我們前邊討論的方法,ApplyOriginalValues
和ApplyCurrentValues
方法屬於ObjectContext
和ObjectSet<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
方法接受所有Added
和Modified
狀態的實體並標記它們為Unchanged
。然後分離所有Deleted
狀態的實體,最後更新ObjectStateManager
條目。
AcceptAllChanges
由ObjectContext
公開,在ObjectSet<T>
類中沒有對用的方法。這就是為什麼需要使用下面的程式碼:
ctx.AcceptAllChanges();
ChangeState
和ChangeObjectState
方法
ChangeState
和ChangeObjectState
方法是靈活的方法。它們允許改變一個實體的狀態到任何其他可能的狀態(Detached
除外)。當使用一個實體時,這些方法非常有用。不過當處理複雜的物件圖時,它們的重要性也增加,這在本章後面討論。
ChangeState
由ObjectStateEntry
類公開,而ChangeObjectState
由ObjectStateManager
類公開。ChangeState
只需要新的狀態,因為ObjectStateEntry
的例項已經指的是一個實體。ChangeObjectState
接受實體和新的狀態作為引數。兩個方法如下面的清單所示:
var osm = ctx.ObjectStateManager;
osm.ChangeObjectState(entity, EntityState.Unchanged);
osm.GetObjectStateEntry(entity).ChangeState(EntityState.Unchanged);
這些方法並不總是物理的更改實體狀態;有時使用先前的方法。例如,改變一個實體的狀態為Unchanged
意味著呼叫ObjectStateEntry
類的AcceptChanges
方法。相反,改變一個實體的狀態從Unchanged
到Added
意味著改變狀態。
有時並不需要實體被持久化或者由上下文追蹤修改。如果那樣,可以將實體從上下文移除。
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
)負責與上下中物件追蹤有關的一切:
- 當新增,附加到上下文或者從上下文中刪除一個實體,實際上是對
state manager
做的這些。 - 當我們說上下文保留從資料庫中讀取的所有實體集合在記憶體中時,其實是
state manager
儲存這些資料。 - 當上下文執行一個身份地圖(
identity-map
)檢查,其實是state manager
執行的檢查。 - 當我們說上下文跟蹤實體間關係式,其實是
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
,OriginalValues
和CurrentValues
。注意OriginalValuesh
和CurrentValues
是DbDataRecord
型別的。
ObjectStateEntry
是抽象類,作為EntityEntry
和RelationshipEntry
的基類。它們都是內部類,所以不能直接操作它們。根據它們的名字,EntityEntry
包含關於實體的資料,RelationshipEntry
包含關於實體間關係的資訊。
EntityKey
屬性很重要,因為它表示state manager
內實體的鍵(key)。
理解state manager
的key
是如何標識物件的?
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
,Modified
和Deleted
狀態中檢索實體並且在日誌中寫入entry
。這個解決方法如下面的清單所示:
第一步是掛鉤SavingChagnes
時間。然後,在處理程式中,使用ObjectStateManager
類的GetObjectEntries
方法檢索特定狀態的所有entry
。它接受一個EntityState
引數要查詢的狀態,返回一個特定狀態所有entry
的集合。如果不同狀態的entry
,可以使用標誌語法組合它們。做種,呼叫logger
方法寫入entry
。
通常,只需要檢索單個entry
。GetObjectStateEntries
在這種情況下不可用。你需要另一個方法,允許傳遞一個實體,得到相對應的狀態管理器(state-manager
)的entry
。state 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 狀態時這個方法也由DeleteObject 和ChangeState 呼叫。 |
SetModified |
標記實體以及它的所有屬性為Modified 。當移動到Modified 時這個方法在內部由ChangeState 呼叫。 |
SetModifiedProperty |
標記一個屬性為Modified ,因此也標記實體。 |
AcceptChanges |
改變實體的狀態為Unchanged 並使用當前值重寫entry 的原值。 |
ChangeState |
改變實體的狀態到輸入值。 |
這些方法使用很簡單,因為它們中的大多數都沒有引數。只有SetModifiedProperty
接收屬性的名稱和ChangeState
接收實體新的狀態。
前面提到由state manager
自動執行的唯一狀態改變是從Unchanged
到Modified
,但它並不總是這樣。下面,深入物件跟蹤機制。
理解物件跟蹤
從技術上來說,state manager
不能監視實體內屬性的修改;當修改發生時,實體通知state manager
。這種通知機制並不總是起作用——它依賴於你如何初始化實體。可以建立下面型別的實體:
- 沒有被代理包裝POCO實體(普通實體)
- 由代理包裝的實體(代理實體)
包裝的實體是一個類,它通過代理啟用擴充套件性。當類的繼承不是封閉的和它的屬性是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-manager
)entry
,並且比較每一個的原值和儲存在實體中的值。當它發現屬性被修改——在本例中,是customer
的name
——它標記屬性為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 manager
為order
,customer
和它們的關聯各建立一個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
看起來如下圖:
當實體是從資料庫中檢索的,在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
)屬性中如何更改關係
假設必須更改關聯有order
的customer
。在兩種情況下可以找到自己:
Customer
已經附加到上下文。如果外來鍵關聯起作用,屬性是同步的,Order
物件變成Modified
。如果使用獨立關聯,實體間只有RelationshipEntry
被建立。Customer
沒有附加到上下文。Customer
在Added
狀態被新增到上下文(記住它不支援部分關係圖)。如果關聯使用外來鍵關聯保持,屬性是同步的並且Order
物件變成Modified
。如果使用獨立關聯,實體間只有RelationshipEntry
被建立。
假設有一個沒有客戶的訂單。如果使用外來鍵關聯,設定外來鍵屬性為null
使order
和customer
的關聯消失。如果使用獨立關聯,設定Customer
屬性為null
,也導致同樣的結果(RelationshipEntry
變成Deleted
)。
記住只有customer
和order
之間的關聯被移除。沒有物件被刪除。
在集合屬性中如何更改關係
集合屬性呼叫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
值,一個新的關係必須定義,外來鍵屬性必須分配到另一個不為空的值,或者不關聯的物件必須刪除。
雖然這看起來像加密了一樣,其實很清晰。detail
的OrderId
屬性不能為null
,因為你不能有獨立的order detail
。detail
必須分配到一個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為什麼不自動刪除實體而是簡單的移除引用產生疑問。答案是在其他情況下這是不正確的行為。想想supplier
和product
的多對多關係。如果是那樣,如果移除由supplier
賣的product
,你不必刪除它。你只需刪除Product-Supplier
表的引用。由於這些不同的行為,EF團隊謹慎地決定讓你顯示選擇怎麼做。
當新增一個實體到集合屬性,你可以在兩種不同的情況下找到自己,依賴於實體是否附加到上下文:
detail
被附加到上下文。如果外來鍵關聯起作用,屬性必須與order
的ID同步。如果使用獨立關聯,只需在實體間建立RelationishipEntry
。detail
沒有附加到上下文。Customer
在Added
狀態(記住,上下文不支援部分關係圖)被新增到上下文。如果關聯使用外來鍵關聯保持,屬性必須與order
的ID保持同步。如果使用獨立關聯,實體間也建立RelationshipEntry
。
有很多的規則,提前瞭解它們可以使操作關係圖簡單點。
更改跟蹤和MergeOption
MergeOption
是ObjectSet<T>
類的一個屬性。它是System.Data.Objects.MergeOption
型別的列舉,包含下列值:
1.
AppendOnly
2.NoTracking
3.OverwriteChanges
4.PreserveChanges
在物件的具體化期間,當使用AppendOnly
(預設設定),state manager
檢查是否已經存在了相同key
的entry
。如果是,返回與entry
相關的實體和放棄來自資料庫的資料。如果沒有,實體被具體化並附加到上下文。這種情況下,state manager
使用具體化的原值和當前值建立entry
。最後,返回具體化的實體。
當使用NoTracking
是,上下文不執行身份地圖檢查,所以來自資料庫的資料總是具體化和返回,即使在state manager
中已經有了相對應的實體。當NoTracking
啟用時,返回的實體處於Detached
狀態,所以上下文不跟蹤它們。
當使用OverwriteChanges
使用時,如果身份地圖檢查在state manager
沒有找到entry
,就具體化實體,附加到上下文並返回。如果entry
找到了,相關的實體狀態設定為Unchanged
,當前值和原值使用來自資料庫中的值更新。
當使用PreserveChanges
是,如果在state manager
中身份地圖檢查沒有找到entry
,就具體化實體,附加到上下文並返回。如果找到了,有以下發生的可能性:
如果實體的狀態是Uchanged
,entry
中的當前值和原值由資料庫的值重寫。實體的狀態仍保持為Unchanged
。
如果實體的狀態是Modified
,修改的屬性的當前值不能被資料庫的值重寫。沒有修改的屬性的原值由資料庫的值重寫。
如果沒有修改的屬性的當前值不同於來自資料庫的值,屬性標記為Modified
。從1.0版本這是一個重大的改變,因為在那個版本的屬性不標記為Modified
。如果需要恢復1.0的行為,設定UseLegacyPreserveChangesBehavior
屬性為true
即可,如下:
ctx.ContextOptions.UseLegacyPreserveChangesBehavior = true;
現在已經瞭解了MergeOption
行為。任何應用程式中,它都是重要的一方面,它也經常會被濫用或者被輕視。
相關文章
- EntityFramework 學習【Entity Lifecycle 實體生命週期】Framework
- react生命週期筆記React筆記
- JSP筆記-生命週期JS筆記
- Vue生命週期的理解Vue
- 理解VUE生命週期Vue
- React生命週期學習筆記React筆記
- Vue的生命週期的理解Vue
- docker筆記23-pod的生命週期Docker筆記
- Vue學習筆記(2)—— Vue的生命週期Vue筆記
- iOS開發筆記(九):UIViewController的生命週期iOS筆記UIViewController
- 新手對React生命週期的理解React
- (譯)理解Rust中的生命週期Rust
- Vue生命週期函式.個人筆記Vue函式筆記
- 理解React-元件生命週期React元件
- ES 筆記三十三: 分片及其生命週期筆記
- 10分鐘理解React生命週期React
- 深入原始碼理解SpringBean生命週期原始碼SpringBean
- React 深入系列4:元件的生命週期React元件
- 個人對vue中生命週期的理解Vue
- 【2】軟體生命週期
- 軟體工程生命週期軟體工程
- 實測Vue生命週期Vue
- Angular生命週期實踐Angular
- View生命週期與Activity生命週期的關係View
- React-生命週期雜記React
- 我對 React v16.4 生命週期的理解React
- 008 Rust死靈書之生命週期Rust
- angular4學習記錄 — 元件通訊、生命週期Angular元件
- 軟體測試生命週期
- vue 基礎入門筆記 09:生命週期函式Vue筆記函式
- .NET Core學習筆記(8)——Entity Framework Core之Database First筆記FrameworkDatabase
- .NET Core學習筆記(9)——Entity Framework Core之Code First筆記Framework
- 軟體測試---BUG的生命週期
- 生命週期
- Entity Framework(1)Framework
- viewController的生命週期ViewController
- Servlet的生命週期Servlet
- UIViewController的生命週期UIViewController