2016 年 4 月

第 31 卷,第 4 期

本文章是由機器翻譯。

資料點 - 處理 EF 中已中斷連線實體的狀態

Julie Lerman

Julie Lerman中斷連接的資料是位於 Entity Framework 的一個老問題並就此而言,大部分的資料存取工具。它從來沒有一個容易解決。伺服器傳送以示運作正常,不知道可能會發生什麼事它在用戶端應用程式要求,甚至不知道是否會傳回的資料。然後突然,有些資料會再次出現在要求中。但是在它的相同的資料? 什麼是最多在它並不存在? 沒有任何項目剛好它嗎? 它是全新的資料? 許多需要擔心 !

身為.NET 開發人員,您可能看到解決此問題的模式。還記得 ADO.NET 資料集嗎? 不僅未包含您的資料,而且它們所封裝的所有每個資料列和每個資料行變更狀態資訊。這不是限制為 「 修改 」 或 「 新 」;原始資料已保留,請一併。當我們開始建置 ASMX Web 服務時,很容易序列化資料集,並將其傳送以示運作正常。如果該訊息會進入.NET 用戶端,該用戶端無法還原序列化資料集,並繼續進行追蹤的變更。當它的時間才能傳回資料服務時,您將只序列化一次,而接著,在伺服器端,將其還原序列化回可愛變更追蹤資訊不變,以輕鬆地保存到資料庫的所有資料集。它的運作。很容易。但它有權這類量持續暴增的資料在網路上來回傳送。不只是資料位元,但取得序列化的資料集的結構建立 big fat XML。

在網路上來回傳送序列化訊息的大小是只有一個問題。Web 服務的好處是,您無法提供服務給各種不同平台,但訊息本身只能以另一個.NET 應用程式有意義。在 2005 中,Scott Hanselman 撰寫好喚醒呼叫 epically 標題為 「 傳回資料集從 web 服務會繁衍的 Satan 和代表所有也就是真正邪惡中 World 」 的問題 (bit.ly/1TlqcB8)。

Entity Framework 取代為主要資料存取工具,在.NET 中的資料集時,所有狀態資訊在網路上的消失不見了。而不是儲存資料,變更追蹤資訊 — 原始值,目前的值,狀態 — ef 儲存做為 ObjectContext 的一部分。但儘管如此,EF 的第一個反覆項目中序列化的實體麻煩的訊息,因為它需要繼承自 EF EntityObject 型別。但實體資料在網路上來回傳送的訊息已遺失了解的狀態。多載的資料集所用的人 freaked。人已熟悉處理中斷連線的狀態已因為其他原因 — EntityObject 基底類別需求。這個問題最後贏得 EF 小組注意 (非常好的事件開啟),與下一個反覆項目,EF4,EF 已經發展到純舊 CLR 物件 (POCO) 支援。也就是類別的 ObjectContext 可維護的一個簡單,而不需該類別是類別的繼承自 EntityObject 狀態。

但在使用 EF4,中斷連線的狀態問題沒有消失。EF 必須不會知道實體的狀態無法追蹤。預期 EF 來提供相同的神奇解決方案人熟悉的資料集,而且已不滿意,就必須選擇之間的輕量級的訊息和中斷連接的變更追蹤。在此同時,開發人員 (包括我) 有探討了很多方法可以通知伺服器其 walkabout 上時,資料發生了什麼事。您可以重新讀取資料庫中的資料,並讓 EF 做比較,以處理已變更的內容,如果任何項目。您無法讓充其量像是 「 如果的識別索引鍵的值為 0,它必須新。 」 您可以在撰寫程式碼來進行探索的相關狀態,以及這些低階 Api 巨魔周圍。我在一天,該後的很多,但這些解決方案沒有滿足。

當 EF4.1 推出的輕量型 DbContext 時,它會具有來自 EF 小組的禮物,能夠輕鬆地通知內容的實體狀態。繼承自 DbContext 的類別,您可以如撰寫程式碼 ︰

myContext.Entity(someEntity).State=EntityState.Modified;

SomeEntity 新內容時,這會強制要開始追蹤的實體,並在相同的時間,指定其狀態的內容。這已足夠 ef 知道何種 SaveChanges 時撰寫 SQL 命令。在上述範例中,它會產生更新命令。Entry()。狀態不會協助解決問題的某些資料來自網路傳輸,但卻能讓您實作現在是由開發人員使用 Entity Framework,我將安排在這篇文章中進一步沿著廣泛使用的好用模式時,了解狀態。

即使 Entity Framework 的下一個版本 — EF 核心 (先前稱為 EF7 的架構) 將會使用中斷連接圖形的較佳的一致性,您將了解本文中的模式也仍然很有用的訣竅可以參考中。

取得來回傳遞資料的圖形,提報中斷連接資料的問題。其中一個最大的問題時,這些圖形包含混合狀態的物件 — 使用有預設無法偵測它收到的實體的不同狀態的伺服器。如果您使用 DbSet.Add,實體將所有取得新增預設標記。如果您使用 DbSet.Attach,它們會標示未變更。即使資料庫的任何資料來源,並填入 [索引鍵屬性,這是大小寫。EF 遵循指示進行,也就是加入或附加。EF 核心會讓我們更新方法,請遵循新增、 附加、 刪除與相同的行為,但將實體標示為已修改。要注意的唯一例外的是,如果 DbContext 已經在追蹤實體,則不會覆寫實體的已知的狀態。但與中斷連線的應用程式中,我不會預期要追蹤任何項目之前從用戶端傳回的資料連接的內容。

測試的預設行為

若要反白顯示問題,讓我們來釐清的預設行為。為了示範,我有具有幾個相關類別 (適用於下載) 的簡單模型 ︰ 忍者、 NinjaEquipment 和氏族。忍者可以有一堆 NinjaEquipment 和單一氏族相關聯。接下來的測試牽涉到新忍者與預先存在、 已取消編輯氏族圖形。請注意,我通常會指派值給 Ninja.ClanId 為了避免混淆與參考資料。事實上,設定外部索引鍵,而不是導覽屬性是可協助您避免許多問題,因為 EF 出狀態使用跨關聯性 「 神奇 」 的作法。(請參閱我在 2013 年 4 月專欄 [bit.ly/20XVxQi],「 為什麼沒有 Entity Framework 插入現有物件到我的資料庫? 」 來深入了解。) 但我在撰寫程式碼示範的 EF 行為的方式。請注意氏族物件都有其索引鍵屬性,識別碼,以指出它是來自資料庫的預先存在的資料填入 ︰

[TestMethod]
public void EFDoesNotComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = new Ninja();
  ninja.Clan = new Clan { Id = 1 };
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    Assert.IsFalse(entries.Any(e => e.State != EntityState.Added));
  }
}

我的 OutputState 方法逐一查看 DbEntityEntry 物件內容會保留每個追蹤實體的狀態資訊的位置,並印出的型別和其狀態的值。

在測試中,我可以模擬案例,某處,我已經建立了新忍者並聯現有氏族。氏族是直接參考資料,並不編輯。然後我會建立新的內容,並告知 EF 來追蹤此圖形使用 DbSet.Add 方法。我沒有正在追蹤的實體是任何判斷提示,但加入。當測試通過時,證明內容並不了解氏族是未變更。測試輸出告訴我認為 EF 兩者的實體會加入 ︰

Result StandardOutput:
Debug Trace:
EF6WebAPI.Models.Ninja:Added
EF6WebAPI.Models.Clan:Added

如此一來,呼叫 SaveChanges 會插入忍者和氏族,導致氏族的複本。如果我有使用 DbSet.Attach 方法,這兩個實體會標示為未變更和 SaveChanges 不會插入新忍者的資料庫,造成實際的資料持續性的問題。

另一個常見案例是從資料庫擷取忍者和其設備,並將它們傳遞至用戶端。用戶端然後編輯其中一個設備的部份,並加入一個新。實體的真實狀態會忍者是不變,一修改並加入另一個。DbSet.Add 或 DbSet.Attach 都不會包含不同的狀態需要一些協助。現在它是套用一些幫助的時間。

告知 EF 的每個實體的狀態

簡單的配方可協助了解每個實體圖形中的正確狀態的 EF 包含四個部分的方案 ︰

  1. 定義列舉,代表物件可能狀態。
  2. 建立介面,以列舉所定義的 ObjectState 屬性。
  3. 在 [網域實體實作的介面。
  4. 讀取物件狀態,並告知 EF DbContext SaveChanges 會覆寫。

EF 有 EntityState 列舉與列舉值未變更、 新增、 修改和刪除。我要建立另一個網域類別所使用的列舉。這個模擬這些四種狀態,但沒有繫結至 Entity Framework Api:

public enum ObjectState
{
  Unchanged,
  Added,
  Modified,
  Deleted
}

未變更的第一個會預設值。如果您想要指定值,請確定未變更等於零 (0)。

接下來,我將建立的介面公開的屬性來追蹤使用此列舉物件的狀態。您可能想要建立基底類別,或將下列加入您已經使用的基底類別 ︰

public interface IObjectWithState
{
  ObjectState State { get; set; }
}

這個狀態屬性只適用於記憶體中,而且不需要保存到資料庫。我已經更新 NinjaContext,以確保實作它的任何物件會忽略此屬性 ︰

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Types<IObjectWithState>().Configure(c => c.Ignore(p=>p.State));
}

定義介面,我可以實作它在類別中,比方說,忍者類別中所示 [圖 1

[圖 1 忍者類別實作 IObjectState

public class Ninja : IObjectWithState
{
  public Ninja() {
    EquipmentOwned = new List<NinjaEquipment>();
  }
  public int Id { get; set; }
  public string Name { get; set; }
  public bool ServedInOniwaban { get; set; }
  public Clan Clan { get; set; }
  public int ClanId { get; set; }
  public List<NinjaEquipment> EquipmentOwned { get; set; }
  public ObjectState State { get; set; }
}

我的預設 ObjectState 列舉定義為未變更,每個忍者就會開始未變更,任何人使用忍者類別撰寫程式碼會負責設定所需的狀態值。

如果用戶端設定狀態的問題,另一個方法,就會影響 Domain-Driven 設計作法,可確保多參與其行為與狀態忍者物件。[圖 2 顯示忍者類別更豐富的定義版本。請注意 ︰

  • 建立 factory 方法都會新增至設定的狀態。
  • 我隱藏的屬性 setter。
  • 我建立了方法來變更的屬性,其中狀態會設為已修改如果不是新忍者 (也就是狀態不已設定為加入)。

[圖 2 是聰明一點忍者類別

public class Ninja : IObjectWithState
{
  public static RichNinja CreateIndependent(string name, 
   bool servedinOniwaban) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public static Ninja CreateBoundToClan(string name,
    bool servedinOniwaban, int clanId) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.ClanId = clanId;
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public Ninja(string name, bool servedinOniwaban) {
    EquipmentOwned = new List<NinjaEquipment>();
    Name = name;
    ServedInOniwaban = servedinOniwaban;
  }
  // EF needs parameterless ctor for queries
  private Ninja(){}
  public int Id { get; private set; }
  public string Name { get; private set; }
  public bool ServedInOniwaban { get; private set; }
  public Clan Clan { get; private set; }
  public int ClanId { get; private set; }
  public List<NinjaEquipment> EquipmentOwned { get; private set; }
  public ObjectState State { get; set; }
  public void ModifyOniwabanStatus(bool served) {
    ServedInOniwaban = served;
    SetModifedIfNotAdded();
  }
  private void SetModifedIfNotAdded() {
    if (State != ObjectState.Added) {
      State = ObjectState.Modified;
    }
  }
  public void SpecifyClan(Clan clan) {
    Clan = clan;
    ClanId = clan.Id;
    SetModifedIfNotAdded();
  }
  public void SpecifyClan(int id) {
    ClanId = id;
    SetModifedIfNotAdded();
  }
  public NinjaEquipment AddNewEquipment(string equipmentName) {
    return NinjaEquipment.Create(Id, equipmentName);
  }
  public void TransferEquipmentFromAnotherNinja(NinjaEquipment equipment) {
    equipment.ChangeOwner(this.Id);
  }
  public void EquipmentNoLongerExists(NinjaEquipment equipment) {
    equipment.State = ObjectState.Deleted;
  }
}

修改後,也是更豐富、 NinjaEquipment 類型和您所見,有什麼好處,AddNew、 傳輸和 NoLongerExists 設備方法中。修改可確保外部索引鍵指向忍者持續正確或在遭到終結的設備,它取得完全從資料庫中刪除這個網域的商務規則。比較麻煩圖形 EF 來重新連線時進行追蹤的關聯性變更,因此我想我可以在網域層級保留嚴格控制的關聯性。比方說,ChangeOwner 方法會將狀態設定為已修改 ︰

public NinjaEquipment ChangeOwner(int newNinjaId) {
  NinjaId = newNinjaId;
  State = ObjectState.Modified;
  return this;
}

現在,無論用戶端明確設定的狀態,或是在用戶端會使用像這樣的類別 (或類似在語言中的用戶端的自動程式碼的類別),傳遞回 API 或服務的物件會定義其狀態。

現在就開始利用伺服器端程式碼中的用戶端狀態。

當我連線物件或物件圖形的內容時,內容必須讀取每個物件的狀態。這個 ConvertState 方法會採用 ObjectState 列舉,並傳回相符的 EntityState 列舉 ︰

public static EntityState ConvertState(ObjectState state) {
  switch (state) {
    case ObjectState.Added:
      return EntityState.Added;
    case ObjectState.Modified:
      return EntityState.Modified;
    case ObjectState.Deleted:
      return EntityState.Deleted;
    default:
      return EntityState.Unchanged;
  }
}

接下來,我需要 NinjaContext 類別,來逐一查看的實體中的方法 — EF 只儲存資料之前,並更新內容的了解每個實體的狀態,根據物件的狀態屬性。FixState 時,會呼叫該方法,如下 ︰

public class NinjaContext : DbContext
{
  public DbSet<Ninja> Ninjas { get; set; }
  public DbSet<Clan> Clans { get; set; }
  public void FixState() {
    foreach (var entry in ChangeTracker.Entries<IObjectWithState>()) {
      IObjectWithState stateInfo = entry.Entity;
      entry.State = DataUtilities.ConvertState(stateInfo.State);
    }
  }
}

我考慮過呼叫 SaveChanges 內從 FixState,如此會完全自動化,但可能有許多案例中的副作用。例如,如果您不擔心設定本機狀態的連線應用程式中使用 IObjectState 實體,FixState 一律會還原為 「 未變更的實體。最好是將做為明確執行的方法。在 「 程式設計 Entity Framework: DbContext,「 共同著作 Rowan Miller 與活頁簿中,我們討論可能會感興趣的一些其他的邊緣案例。

現在,我要建立新版本會使用這些新功能,包括我的類別更豐富的新版測試中前一個測試。新的測試會判斷提示 EF comprehends 混合的狀態,如繫結至現有的氏族全新忍者。之前和之後呼叫 NinjaContext.FixState 測試方法就會列印出 EntityState:

[TestMethod]
public void EFComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = Ninja.CreateIndependent("julie", true);
  ninja.SpecifyClan(new  Clan { Id = 1, ClanName = "Clan from database" });
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    context.FixState();
    OutputState(entries);
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Unchanged));
}

測試成功,輸出會顯示 FixState 方法,套用至氏族的適當狀態。如果我呼叫 SaveChanges,該氏族不會重新插入資料庫錯誤 ︰

Debug Trace:
Before:EF6Model.RichModels.Ninja:Added
Before:EF6Model.RichModels.Clan:Added
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.Clan:Unchanged

使用此模式也會解決我討論過早其中忍者可能尚未編輯和任意數目的變更 (插入、 修改或刪除) 設備忍者圖形的問題。[圖 3 顯示檢查以查看是否 EF 正確地識別出其中一個項目已修改的測試。

[圖 3 測試圖形中的子系的狀態

[TestMethod]
public void MixedStatesWithExistingParentAndVaryingChildrenisUnderstood() {
  // Arrange
    var ninja = Ninja.CreateIndependent("julie", true);
    var pNinja =new PrivateObject(ninja);
    pNinja.SetProperty("Id", 1);
    var originalOwnerId = 99;
    var equip = Create(originalOwnerId, "arrow");
  // Act
    ninja.TransferEquipmentFromAnotherNinja(equip);
    using (var context = new NinjaContext()) {
      context.Ninjas.Attach(ninja);
      var entries = context.ChangeTracker.Entries();
      OutputState(entries);
      context.FixState();
      OutputState(entries);
  // Assert 
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Modified));
  }
}

測試成功,輸出會顯示原始的附加方法會導致所有標示為未變更的物件。呼叫 FixState、 忍者後未變更 (這是仍然正確),但設備物件已正確設定已修改為 ︰

Debug Trace:
Before:EF6Model.RichModels.Ninja:Unchanged
Before:EF6Model.RichModels.NinjaEquipment:Unchanged
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.NinjaEquipment:Modified

EF 核心呢?

即使當我移至 EF 核心,我將我的工具箱中保留這種模式。成果已有向簡化圖形中斷連線的問題 — 大部分順著提供一致的模式。在將狀態設定使用 DbContext.Entry() EF 核心。State 屬性只會設定狀態的圖形的根。這會在許多情況下很有用。此外,還有新的方法呼叫,將 「 查核圖形,「 TrackGraph 達到內,每個實體,並將指定的函式套用至每個方法。最明顯的函式是直接設定狀態 ︰

context.ChangeTracker.TrackGraph(Samurai_GK,
  e => e.Entry.State = EntityState.Added);

想像您是使用上述的 FixState 方法來套用根據用戶端設定 ObjectState EF 狀態的其中一個該函式。

豐富網域模型簡化控制在用戶端的狀態

而我希望建置更豐富的網域類別,視需要更新的狀態,您就可以達到簡單 CRUD 類別相同的結果,只要用戶端使用的類別明確地設定狀態。以手動方式,不過,您必須特別注意已修改的關聯性,確保您帳戶的外部索引鍵的修改。

我已經使用這種模式,多年來,並共用中的書籍,在研討會發表演說,與用戶端,並在 Pluralsight 課程中。而且我知道它好消息是用於許多軟體解決方案。無論您是使用 EF5 或 EF6,或是 gearing EF 核心,此配方應該移除龐大的圖層的中斷連接的資料與相關的痛苦。

自我追蹤實體

EF4.1 的另一個功能是產生 「 自我追蹤實體 」,這些新釋放的 POCOs 轉換回權衡下野獸的 T4 範本。自我追蹤實體原意是專為 Windows Communication Foundation (WCF) 服務,提供資料給.NET 用戶端的案例。我從未自我追蹤實體風扇,快樂時以安靜模式從 EF 消失。不過,有些開發人員依賴它們。而且有一些 Api 可為您提供這些優點。例如,Tony Sneed 建置名為 「 trackable 實體 」,您可以在找到的輕量型實作 trackableentities.github.io。IdeaBlade (ideablade.com) 具有豐富的經驗解決其旗艦產品 DevForce EF 支援中斷連接的資料的問題。IdeaBlade 會接受該知識,並 Breeze.js 和幫助您輕鬆 # 產品,提供用戶端和伺服器端狀態追蹤,以及建立免費的開放原始碼。我所發表的幫助您輕鬆先前在本專欄中,在 2012 年 12 月 (bit.ly/1WpN0z3) 和 2014 年 4 月問題 (bit.ly/1Ton1Kg)。


Julie Lerman是 Microsoft MVP、.net 和顧問 Vermont 山區中。您可以找到她針對資料存取和使用者群組和世界各地的研討會其他.NET 主題呈現。她的部落格網址 thedatafarm.com /blog 以及 Code First DbContext 版本中的,所有從 O'Reilly Media 是 「 程式設計 Entity framework 」。在 Twitter 上追隨她 ︰ @julielerman ,請參閱在她 Pluralsight 課程 juliel.me/PS 影片

感謝以下的微軟技術專家對本文的審閱: Rowan Miller