2016-11-11 14 views
1

高性能と低遅延が不可欠なかなり複雑なエンティティモデルがありますが、水平スケーラビリティは必要ありません。アプリケーションには、自己ホスト型のASP.NET Web APIに加えていくつかのイベントソースがあります。2. Entity Framework 6を​​使用してPOCOクラスからデータベースにマップします(クラスを生成するために優秀なReverse POCO Generatorを使用します)。Entity Framework - 読み取り専用オブジェクトをキャッシュして共有する方法

イベントが到着するたびに、アプリケーションはエンティティモデルを調整し、EFを介してこのデルタ調整をデータベースに保持する必要があります。同時に、読取り要求または更新要求がWeb APIを介して到着することがあります。

モデルには多くのテーブルとFK関係があり、イベントに反応するためには通常、対象エンティティのすべての関係がロードされている必要があるため、データセット全体をロードするのではなくメモリ内キャッシュに保持することにしました各イベントのオブジェクトグラフ全体。プログラム起動時に

enter image description here

我々は一時的DbContextを経由して、すべての興味深いClassAのインスタンス(およびそれに関連する依存関係グラフ)をロードし、(辞書に挿入 - は:下の画像は、我々のモデルの単純化されたバージョンを示してい私たちのキャッシュ)。イベントが到着すると、ClassAインスタンスがキャッシュに格納され、イベントごとにDbContextDbSet.Attach())にアタッチされます。プログラムはawait-asyncパターンを使用して書き込まれ、複数のイベントを同時に処理できます。ロックを使用してキャッシュされたオブジェクトが同時にアクセスされるのを防ぎます。したがって、キャッシュされたClassAを1度に1つずつDbContextにロードできることが保証されます。これまでのところ、とても優れていて、パフォーマンスは優れており、我々はそのメカニズムに満足しています。 しかし、に問題があります。エンティティグラフはClassAの下にかなり自己完結していますが、読み取り専用のスタティックデータ(イメージのオレンジ色で網掛けされている)を表すPOCOクラスがいくつかあります。我々は、EFが時々不平を言うことを発見しました。

IEntityChangeTrackerの複数のインスタンスでエンティティオブジェクトを参照することはできません。

彼らは同じClassATypeへの参照を共有しているため、我々は(我々は異なるDbcontextsに添付されていても)同時にClassAAttach() 2つの異なるインスタンスにしようとしたとき。これは、以下のコードによって実証されています -

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 

ClassATypeは/静的読み取り専用であり、我々はそれが、各インスタンスは一つだけにロードすることができることを保証したくないというEFを通知する方法はありますDbContext?これまで私が見つけた問題を回避する唯一の方法は、これらのFK関係を無視するようにPOCOジェネレータを変更することです。そのため、エンティティモデルの一部ではありません。しかし、これは、静的データへのアクセスを必要とする処理方法がClassAであるため、プログラミングを複雑にする。

私はこれが働くかもしれないと思う

答えて

0

が、私はこの質問への鍵は正確に例外が何を意味するかだと思う: -

エンティティオブジェクトがIEntityChangeTrackerの複数のインスタンスによって参照することはできません。

おそらく、この例外は、Entity Frameworkのオブジェクトのインスタンスを複数に変更されたことを不平を言っているDbContextsではなく、単に複数のDbContexts内のオブジェクトによって参照されていることを私に起こりました。私の理論は、生成されたPOCOクラスが逆FKナビゲーションプロパティを持ち、エンティティグラフをDbContextに添付するプロセスの一部としてこれらの逆方向ナビゲーションプロパティを自然に修復しようとしているという事実に基づいています(a description of the fix-up process参照)

この理論をテストするために、簡単なテストプロジェクトを作成しました。ここで、逆のナビゲーションプロパティを有効または無効にできます。私の大きな喜びに私は理論が正しいことを発見しました。オブジェクト自体が変更されない限り、EFはオブジェクトが複数回参照されることが非常にうれしく思います。これにはナビゲーションプロパティが含まれています fix- 。

そこで質問に対する答えは単純に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

関連する問題