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

Posted

技术标签:

【中文标题】实体框架 - 如何缓存和共享只读对象【英文标题】:Entity Framework - how to cache and share read-only objects 【发布时间】:2017-03-25 17:51:02 【问题描述】:

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

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

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

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

一个实体对象不能被多个 IEntityChangeTracker 实例引用。

当我们尝试同时Attach() 两个不同的ClassA 实例时(即使我们附加到不同的Dbcontexts),因为它们共享对同一个ClassAType 的引用。下面的代码 sn-p 证明了这一点:-

    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 中有处理方法需要访问静态数据。

【问题讨论】:

【参考方案1】:

我认为这个问题的关键是异常究竟意味着什么:-

一个实体对象不能被多个 IEntityChangeTracker 实例引用。

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

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

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

确保 静态数据 对象永远不会更改(理想情况下,它们应该没有公共 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());
    

【讨论】:

【参考方案2】:

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

dbContext.ClassEType.AsNoTracking();

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

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

【讨论】:

嗨@戴安娜。我认为这不会有帮助,原因有几个。 1) 如果我使用AsNoTracking,那么DbContext 不会缓存对象,因此ClassA 图形的后续加载将需要再次包含它们。 2)我认为实体的跟踪状态是DbContext的属性而不是对象本身,所以当原始DbContext被处理时,不会有跟踪/不跟踪的残余内存。 你是对的,实体将与DbContext 分离,它们不会有Entry。然后我猜你的解决方案是为每个不同的ClassA 图创建这些实体的新实例,而不是使用相同的实例。类似于在将它们分配给图表之前克隆它们。 我相信沿着这些思路的东西会起作用,但我认为这对加载实体图的代码来说是一个重大的复杂性(它必须迭代所有 ClassAs 并替换只读类实例——在结构中相当深入)。我希望在静态数据类的级别上能有更多的声明性。 如果您有自定义代码来加载实体图,也许您可​​以找到一种不需要搜索整个图和替换的方法。例如,只获取这些实体的 ID 而不是导航属性,然后您填充它但从您自己的列表中获取一个克隆(就像一种不访问真实数据库但访问您本地克隆的实体实例的延迟加载)。像覆盖 YourDbContext.Local 之类的东西?我什至不知道这是否可能。抱歉,我无法提供真正的解决方案,只是想提出一些疯狂的想法。 非常感谢您的建议。通过忽略(即不生成)指向ClassEType 的导航属性,我已经沿着这条路径走了一部分,所以ClassE 必须使用一些访问它的ClassEType 实例其他机制(我正在考虑在ClassEType 上创建一个静态集合属性)

以上是关于实体框架 - 如何缓存和共享只读对象的主要内容,如果未能解决你的问题,请参考以下文章

如何让asp.net成员提供者和实体框架共享同一个数据库

实体框架代码优先:共享主键

蓝牙共享的音频文件只读

共享主机 + 实体框架 + MySQL = 数据提供者错误?

将只读属性分配给实体框架中的新对象

集成测试共享数据库的多个实体框架 dbcontexts