实体框架 - 如何缓存和共享只读对象

时间:2016-11-11 13:25:47

标签: c# entity-framework caching entity-framework-6 dbcontext

我们的应用程序具有相当复杂的实体模型,其中高性能和低延迟是必不可少的,但我们不需要水平可伸缩性。除了自托管的ASP.NET Web API 2之外,该应用程序还有许多事件源。我们使用Entity Framework 6从POCO类映射到数据库(我们使用优秀的Reverse POCO Generator来生成我们的类)

每当事件到达时,应用程序必须对实体模型进行一些调整,并通过EF将此增量调整持久保存到数据库。同时,读取或更新请求可以通过Web API到达。

因为模型涉及许多表和FK关系,并且对事件做出反应通常需要加载主题实体下的所有关系,所以我们选择将整个数据集维护在内存缓存中而不是加载每个事件的整个对象图。下图显示了我们模型的简化版本: -

enter image description here

在程序启动时,我们通过临时ClassA加载所有有趣的DbContext实例(及其相关的依赖图)并插入到Dictionary(即我们的缓存)中。当事件到达时,我们会在缓存中找到ClassA实例,并通过DbContext将其附加到每个事件DbSet.Attach()。程序是使用await-async模式编写的,并且可以同时处理多个事件。我们通过使用锁来保护缓存对象不被同时访问,因此我们保证缓存的ClassA只能一次加载到DbContext。到目前为止,性能非常好,我们对机制感到满意。 但是存在问题。尽管实体图在ClassA下是相当自包含的,但是有一些POCO类表示我们认为是只读的静态数据(图像中以橙色阴影显示)。我们发现EF有时会抱怨

  

IEntityChangeTracker的多个实例无法引用实体对象。

当我们同时尝试Attach()两个不同的ClassA实例时(即使我们附加到不同的Dbcontexts),因为它们共享对同一{{1}的引用}}。下面的代码片段对此进行了演示: -

ClassAType

有没有办法通知EF 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个?到目前为止,解决问题的唯一方法是修改POCO生成器以忽略这些FK关系,因此它们不是实体模型的一部分。但这使编程变得复杂,因为DbContext中有处理方法需要访问静态数据。

2 个答案:

答案 0 :(得分:0)

我认为这可能会有效:在程序启动时选择DbSets时,请尝试在这些实体中使用AsNoTracking

dbContext.ClassEType.AsNoTracking();

这将禁用它们的更改跟踪,因此EF不会尝试保留它们。

此外,这些实体的POCO类应该只具有只读属性(没有set方法)。

答案 1 :(得分:0)

我认为这个问题的关键在于异常意味着什么: -

  

IEntityChangeTracker的多个实例无法引用实体对象。

我想到,也许这个例外是Entity Framework抱怨一个对象的实例已在多个DbContexts中被更改,而不是简单地被多个DbContexts中的对象引用。我的理论基于以下事实:生成的POCO类具有反向FK导航属性,并且实体框架自然会尝试修复这些反向导航属性,作为将实体图附加到DbContext的过程的一部分。 (见a description of the fix-up process

为了测试这个理论,我创建了一个简单的测试项目,我可以启用和禁用反向导航属性。令我非常高兴的是,我发现理论是正确的,并且只要对象本身不会改变,EF就很乐意多次引用对象 - 这包括更改的导航属性通过修复过程。

所以问题的答案只是遵循两条规则: -

  • 确保静态数据对象永远不会更改(理想情况下,它们应该没有公共 setter 属性)和
  • 不要包含指向引用类的任何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());
    }
}