C# 实体框架-如何缓存和共享只读对象
我们有一个具有相当复杂实体模型的应用程序,其中高性能和低延迟至关重要,但我们不需要横向可伸缩性。除了自托管的ASP.NET Web API 2之外,该应用程序还有许多事件源。我们使用EntityFramework6将POCO类映射到数据库(我们使用优秀的生成类) 每当事件到达时,应用程序必须对实体模型进行一些调整,并通过EF将此增量调整持久化到数据库中。同时,读取或更新请求可能通过Web API到达 由于模型涉及许多表和FK关系,并且对事件作出反应通常需要加载主题实体下的所有关系,因此我们选择在内存缓存中维护整个数据集,而不是为每个事件加载整个对象图。下图显示了我们模型的简化版本:- 在程序启动时,我们通过一个临时的C# 实体框架-如何缓存和共享只读对象,c#,entity-framework,caching,entity-framework-6,dbcontext,C#,Entity Framework,Caching,Entity Framework 6,Dbcontext,我们有一个具有相当复杂实体模型的应用程序,其中高性能和低延迟至关重要,但我们不需要横向可伸缩性。除了自托管的ASP.NET Web API 2之外,该应用程序还有许多事件源。我们使用EntityFramework6将POCO类映射到数据库(我们使用优秀的生成类) 每当事件到达时,应用程序必须对实体模型进行一些调整,并通过EF将此增量调整持久化到数据库中。同时,读取或更新请求可能通过Web API到达 由于模型涉及许多表和FK关系,并且对事件作出反应通常需要加载主题实体下的所有关系,因此我们选择在
DbContext
加载所有感兴趣的ClassA
实例(及其相关的依赖关系图),并将其插入字典(即缓存)。当事件到达时,我们在缓存中找到ClassA实例,并通过DbSet.attach()
将其附加到每个事件DbContext
。整个程序都是使用wait异步模式编写的,可以同时处理多个事件。我们通过使用锁来保护缓存对象不被并发访问,因此我们保证缓存的ClassA
只能一次加载到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
concurrentCache=null;
使用(var ctx=new MyDbContext())
{
var classAs=ctx.classAs
.Include(a=>a.ClassAType)
.ToList();
theCache=新的ConcurrentDictionary(classAs.ToDictionary(a=>a.ID));
}
//取引用同一ClassA类型的ClassA的两个不同实例
//并将它们加载到单独的DBContext中
var ctx1=新的MyDbContext();
ctx1.ClassAs.Attach(缓存[1]);
var ctx2=新的MyDbContext();
ctx2.ClassAs.Attach(缓存[2]);//这里抛出异常
有没有办法通知EF
ClassAType
是只读/静态的,我们不希望它确保每个实例只能加载到一个DbContext
?到目前为止,我发现解决这个问题的唯一方法是修改POCO生成器以忽略这些FK关系,因此它们不是实体模型的一部分。但这会使编程复杂化,因为ClassA
中有一些处理方法需要访问静态数据。我认为这可能会起作用:在程序启动时选择这些实体数据库集时,尝试在这些实体数据库集中使用AsNoTracking
:
dbContext.ClassEType.AsNoTracking();
这将禁用对它们的更改跟踪,因此EF不会尝试持久化它们
此外,这些实体的POCO类应该只有只读属性(没有
set
方法)。我认为这个问题的关键是异常的确切含义:-
一个实体对象不能被多个IEntityChangeTracker实例引用
我突然想到,这个例外可能是实体框架抱怨一个对象的实例在多个DbContexts
中被更改,而不是简单地被多个DbContexts
中的对象引用。我的理论基于这样一个事实:生成的POCO类具有反向FK导航属性,实体框架自然会尝试修复这些反向导航属性,作为将实体图附加到DbContext
过程的一部分(请参阅)
为了验证这个理论,我创建了一个简单的测试项目,可以在其中启用和禁用反向导航属性。令我非常高兴的是,我发现这个理论是正确的,而且只要对象本身不发生变化,EF就非常乐意多次引用对象,这包括通过修复过程改变导航属性
因此,这个问题的答案只是遵循两条规则:
- 确保静态数据对象从未更改(理想情况下,它们不应具有公共setter属性),并且
- 不要包含任何指向引用类的FK反向导航属性。对于的用户,我建议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));
}
}
类程序
{
静态void Main(字符串[]参数)
{
ConcurrentDictionary theCache=null;
尝试
{
使用(var ctx=new MyDbContext())
{
var classAs=ctx.classAs
.Include(a=>a.ClassAType)
.ToList();
theCache=新的ConcurrentDictionary(classAs.ToDictionary(a=>a.ID));
}
//以引用同一ClassA类型的2个ClassA实例为例
//并将它们加载到单独的DBContext中
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());
}
}