2016-11-11 48 views
1

我們有一個相當複雜的實體模型的應用程序,其中高性能和低延遲是必不可少的,但我們不需要橫向可伸縮性。除了自託管的ASP.NET Web API 2之外,該應用程序還有許多事件源。我們使用Entity Framework 6將POCO類映射到數據庫(我們使用優秀的Reverse POCO Generator來生成我們的類)。實體框架 - 如何緩存和共享只讀對象

每當事件到達時,應用程序必須對實體模型進行一些調整,並通過EF持續對數據庫進行增量調整。同時,讀取或更新請求可能通過Web API到達。因爲該模型涉及許多表和FK關係,並且對事件作出反應通常需要加載該實體下的所有關係,所以我們選擇將整個數據集維護在內存中緩存而不是加載每個事件的整個對象圖。下圖顯示了我們的模型的簡化版本: -

enter image description here

在程序啓動時,我們通過臨時DbContext加載所有有趣的ClassA實例(及其關聯依賴圖),並插入到一個詞典(即我們的緩存)。當事件到達時,我們在緩存中找到ClassA實例,並通過DbSet.Attach()將其附加到每個事件DbContext。該程序使用await-async模式編寫,並且可以同時處理多個事件。我們通過使用鎖來保護緩存的對象不被同時訪問,所以我們保證緩存的對象只能一次加載到DbContext中。迄今爲止,表現非常出色,我們對該機制感到滿意。 但是有一個問題。雖然實體圖在ClassA下相當獨立,但有些POCO類表示我們認爲是隻讀靜態數據(圖像中以橙色陰影)。我們發現EF有時會抱怨

IEntityChangeTracker的多個實例無法引用實體對象。

,當我們試圖Attach()兩個不同的ClassA情況下在同一時間(即使我們連接到不同的Dbcontexts),因爲它們共享相同的ClassAType的參考。

ConcurrentDictionary<int,ClassA> theCache = null; 

    using(var ctx = new MyDbContext()) 
    { 
     var classAs = ctx.ClassAs 
      .Include(a => a.ClassAType) 
      .ToList(); 

     theCache = new ConcurrentDictionary<int,ClassA>(classAs.ToDictionary(a => a.ID)); 
    } 

    // take 2 different instances of ClassA that refer to the same ClassAType 
    // and load them into separate DbContexts 

    var ctx1 = new MyDbContext(); 
    ctx1.ClassAs.Attach(theCache[1]); 

    var ctx2 = new MyDbContext(); 
    ctx2.ClassAs.Attach(theCache[2]); // exception thrown here 

有什麼辦法告知EF是ClassAType只讀/靜態的,我們不希望它確保每個實例可以加載到只有一個 - :這是由下面的代碼片段演示DbContext?到目前爲止,解決問題的唯一方法是修改POCO生成器以忽略這些FK關係,因此它們不是實體模型的一部分。但是這會使編程複雜化,因爲需要訪問靜態數據的處理方法有ClassA

回答

0

我認爲,關鍵這個問題是完全例外的意思: -

一個實體對象不能被IEntityChangeTracker的多個實例的引用。

它發生,我認爲這也許例外是實體框架抱怨的對象的實例已在多個被更改DbContexts而不是簡單地通過物體在多個DbContexts引用。我的理論基於這樣的事實:生成的POCO類具有反向的FK導航屬性,並且實體框架自然會試圖修復這些反向導航屬性,作爲將實體圖附加到DbContext的過程的一部分(請參閱a description of the fix-up process

爲了測試這個理論,我創建了一個簡單的測試項目,我可以啓用和禁用反向導航屬性。我非常高興地發現理論是正確的,只要對象本身沒有改變,EF很樂意讓對象被多次引用 - 這包括導航屬性被修改爲, up過程。

所以,這個問題的答案是簡單地按照2個規則: -

  • 確保靜態數據對象是從未改變(理想情況下,他們應該沒有公共二傳手屬性)和
  • 待辦事項不包括任何指向引用類的FK反向導航屬性。對於Reverse POCO Generator的用戶,我向Simon Hughes(作者)提出了添加增強功能的建議,使其成爲配置選項。

我已經包含了測試類如下: -

class Program 
{ 
    static void Main(string[] args) 
    { 
     ConcurrentDictionary<int,ClassA> theCache = null; 

     try 
     { 
      using(var ctx = new MyDbContext()) 
      { 
       var classAs = ctx.ClassAs 
        .Include(a => a.ClassAType) 
        .ToList(); 

       theCache = new ConcurrentDictionary<int,ClassA>(classAs.ToDictionary(a => a.ID)); 
      } 

      // take 2 instances of ClassA that refer to the same ClassAType 
      // and load them into separate DbContexts 
      var classA1 = theCache[1]; 
      var classA2 = theCache[2]; 

      var ctx1 = new MyDbContext(); 
      ctx1.ClassAs.Attach(classA1); 

      var ctx2 = new MyDbContext(); 
      ctx2.ClassAs.Attach(classA2); 

      // When ClassAType has a reverse FK navigation property to 
      // ClassA we will not reach this line!  

      WriteDetails(classA1); 
      WriteDetails(classA2); 

      classA1.Name = "Updated"; 
      classA2.Name = "Updated"; 

      WriteDetails(classA1); 
      WriteDetails(classA2); 
     } 
     catch(Exception ex) 
     { 
      Console.WriteLine(ex.Message); 
     } 
     System.Console.WriteLine("End of test"); 
    } 

    static void WriteDetails(ClassA classA) 
    { 
     Console.WriteLine(String.Format("ID={0} Name={1} TypeName={2}", 
      classA.ID, classA.Name, classA.ClassAType.Name)); 
    } 
} 

public class ClassA 
{ 
    public int ID { get; set; } 
    public string ClassATypeCode { get; set; } 
    public string Name { get; set; } 

    //Navigation properties 
    public virtual ClassAType ClassAType { get; set; } 
} 

public class ClassAConfiguration : System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<ClassA> 
    { 
    public ClassAConfiguration() 
     : this("dbo") 
    { 
    } 

    public ClassAConfiguration(string schema) 
    { 
     ToTable("TEST_ClassA", schema); 
     HasKey(x => x.ID); 

     Property(x => x.ID).HasColumnName(@"ID").IsRequired().HasColumnType("int").HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity); 
     Property(x => x.Name).HasColumnName(@"Name").IsRequired().HasColumnType("varchar").HasMaxLength(50); 
     Property(x => x.ClassATypeCode).HasColumnName(@"ClassATypeCode").IsRequired().HasColumnType("varchar").HasMaxLength(50); 

     //HasRequired(a => a.ClassAType).WithMany(b => b.ClassAs).HasForeignKey(c => c.ClassATypeCode); 
     HasRequired(a => a.ClassAType).WithMany().HasForeignKey(b=>b.ClassATypeCode); 
    } 
} 

public class ClassAType 
{ 
    public string Code { get; private set; } 
    public string Name { get; private set; } 
    public int Flags { get; private set; } 


    // Reverse navigation 
    //public virtual System.Collections.Generic.ICollection<ClassA> ClassAs { get; set; } 
} 

public class ClassATypeConfiguration : System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<ClassAType> 
    { 
    public ClassATypeConfiguration() 
     : this("dbo") 
    { 
    } 

    public ClassATypeConfiguration(string schema) 
    { 
     ToTable("TEST_ClassAType", schema); 
     HasKey(x => x.Code); 

     Property(x => x.Code).HasColumnName(@"Code").IsRequired().HasColumnType("varchar").HasMaxLength(12); 
     Property(x => x.Name).HasColumnName(@"Name").IsRequired().HasColumnType("varchar").HasMaxLength(50); 
     Property(x => x.Flags).HasColumnName(@"Flags").IsRequired().HasColumnType("int"); 

    } 
} 

public class MyDbContext : System.Data.Entity.DbContext 
{ 
    public System.Data.Entity.DbSet<ClassA> ClassAs { get; set; } 
    public System.Data.Entity.DbSet<ClassAType> ClassATypes { get; set; } 

    static MyDbContext() 
    { 
     System.Data.Entity.Database.SetInitializer<MyDbContext>(null); 
    } 

    const string connectionString = @"Server=TESTDB; Database=TEST; Integrated Security=True;"; 

    public MyDbContext() 
     : base(connectionString) 
    { 
    } 

    protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) 
    { 
     base.OnModelCreating(modelBuilder); 
     modelBuilder.Configurations.Add(new ClassAConfiguration()); 
     modelBuilder.Configurations.Add(new ClassATypeConfiguration()); 
    } 
} 
0

我想這可能工作:儘量在這些實體DbSets使用AsNoTracking在程序選擇它們時啓動:

dbContext.ClassEType.AsNoTracking(); 

這將禁用的變化對他們的跟蹤,所以EF不會嘗試堅持他們。

此外,這些實體的POCO類應該只具有隻讀屬性(沒有set方法)。

+0

嗨@Diana。我不認爲這有助於幾個原因。 1)如果我使用'AsNoTracking',那麼這些對象將不會被'DbContext'緩存,所以'ClassA'圖的後續加載將需要再次包含它們。 2)我相信實體的跟蹤狀態是DbContext的屬性,而不是對象本身,所以當原始DbContext被放置時,不會有跟蹤/不跟蹤的剩餘內存。 – Rob

+0

你是對的,實體將從'DbContext'中分離出來,那麼它們就不會有'Entry'。然後我想你的解決方案將爲每個不同的'ClassA'圖創建這些實體的新實例,而不是使用相同的實例。就像在將它們分配給圖之前克隆它們一樣。 – Diana

+0

我相信沿着這些路線的東西可以工作,但我認爲這將是加載實體圖的代碼的一個重大複雜化(它將不得不迭代所有的ClassA並替換隻讀的類實例 - 這些實例非常深入結構)。我希望在靜態數據類的層次上有更多的聲明。 – Rob