EF6使用命令树拦截器禁用查询计划缓存

我正在使用IDbCommandTreeInterceptor来实现软删除function。 在标准的TreeCreated方法中,我检查给定的查询命令是否包含具有soft-delete属性的模型。 如果他们这样做并且用户也要求获取软删除的对象 – 我用querySoftDeleted = true调用我的软删除访问者。 这将使我的查询返回所有对象,那些具有trueIsDeleted属性具有false值的对象。

 public class SoftDeleteInterceptor : IDbCommandTreeInterceptor { public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext) { ... bool shouldFetchSoftDeleted = context != null && context.ShouldFetchSoftDeleted; this.visitor = new SoftDeleteQueryVisitor(ignoredTypes, shouldFetchSoftDeleted); var newQuery = queryCommand.Query.Accept(this.visitor); ... } } public class SoftDeleteQueryVisitor { ... public override DbExpression Visit(DbScanExpression expression) { // Skip filter if all soft deleted items should be fetched if (this.shouldFetchSoftDeleted) return base.Visit(expression); ... // TODO Apply `IsDeleted` filter. } } 

当我尝试检索所有对象(也是软删除)然后使用相同的查询后来的对象时,问题就出现了。 像这样的东西:

 context.ShouldFetchSoftDeleted = true; var retrievedObj= context.Objects.Find(obj.Id); 

然后在新的上下文实例中(不在相同的上下文中)

 var retrievedObj= context.Objects.Find(obj.Id); 

第二次, ShouldFetchSoftDeleted设置为false,一切都很好,但EF决定此查询与之前的查询相同,并从缓存中检索它。 检索到的查询不包含filter,因此返回所有对象(软删除而不是)。 处理上下文时不会清除缓存。

现在的问题是,理想情况下,是否有一种方法可以标记构造的DbCommand以便它不会被缓存。 可以这样做吗? 或者有没有办法强制查询重新编译?

有一些方法可以避免缓存,但我宁愿不必更改应用程序中的每个查询来解决这个问题。

有关查询计划缓存的更多信息,请访问此处 。

编辑1

我正在为每个请求使用新的上下文 – 对象缓存不应该是问题。

编辑2

这是数据库日志。 第一次调用是软删除,第二次是w / o。 ...部分相同,所以我将它们从日志中排除。 您可以看到两个请求都是相同的。 第一个调用CreateTree并且结果树被缓存,以便在执行时从缓存中检索树,并且在应该的时候不会重新应用我的软删除标志。

 Opened connection at 16.5.2015. 2:34:25 +02:00 SELECT [Extent1].[Id] AS [Id], [Extent1].[IsDeleted] AS [IsDeleted], ... FROM [dbo].[Items] AS [Extent1] WHERE [Extent1].[Id] = @p__linq__0 -- p__linq__0: '1' (Type = Int64, IsNullable = false) -- Executing at 16.5.2015. 2:34:25 +02:00 -- Completed in 22 ms with result: SqlDataReader Closed connection at 16.5.2015. 2:34:25 +02:00 The thread 0x1008 has exited with code 259 (0x103). The thread 0x1204 has exited with code 259 (0x103). The thread 0xf94 has exited with code 259 (0x103). Opened connection at 16.5.2015. 2:34:32 +02:00 SELECT [Extent1].[Id] AS [Id], [Extent1].[IsDeleted] AS [IsDeleted], ... FROM [dbo].[Items] AS [Extent1] WHERE [Extent1].[Id] = @p__linq__0 -- p__linq__0: '1' (Type = Int64, IsNullable = false) -- Executing at 16.5.2015. 2:34:32 +02:00 -- Completed in 16 ms with result: SqlDataReader Closed connection at 16.5.2015. 2:34:32 +02:00 'vstest.executionengine.x86.exe' (CLR v4.0.30319: UnitTestAdapter: Running test): Loaded 'C:\Windows\assembly\GAC_MSIL\Microsoft.VisualStudio.DebuggerVisualizers\12.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.DebuggerVisualizers.dll'. Cannot find or open the PDB file. 

正如我已经说过的,我在自己的上下文中执行了每个请求,如下所示:

  using (var context = new MockContext()) { // Test overrided behaviour // This should return just deleted entity // Enable soft-delete retrieval context.ShouldFetchSoftDeleted = true; // Request 1 goes here // context.Items.Where(...).ToList() } using (var context = new MockContext()) { // Request 2 goes here // context.Items.Where(...).ToList() } 

区分查询计划缓存结果缓存很重要:

entity framework中的缓存

查询计划缓存

第一次执行查询时,它会通过内部计划编译器将概念查询转换为存储命令(例如,针对SQL Server运行时执行的T-SQL)。 如果启用了查询计划缓存,则下次执行查询时,将直接从查询计划缓存中检索存储命令以执行,从而绕过计划编译器。

查询计划缓存在同一AppDomain中的ObjectContext实例之间共享。 您不需要保留ObjectContext实例以从查询计划缓存中受益。

查询缓存是优化的SQL指令计划。 这些计划有助于使EF查询比“冷”查询更快。 这些计划超出了特定的范围。

对象缓存:

默认情况下,在查询结果中返回实体时,就在EF实现它之前,ObjectContext将检查具有相同键的实体是否已加载到其ObjectStateManager中。 如果具有相同密钥的实体已存在,则EF将其包含在查询结果中。 尽管EF仍将针对数据库发出查询,但此行为可以绕过实现多次实体化的大部分成本。

换句话说,对象缓存是结果缓存的一种软forms。 除非您明确包含,否则entity framework不提供其他类型的二级缓存。 entity framework和Azure中的二级缓存

AsNoTracking

返回一个新查询,其中返回的实体不会缓存在DbContext或ObjectContext中

 Context.Set().AsNoTracking(); 

或者,您可以使用MergeOption NoTracking Option禁用实体的对象缓存:

不会修改缓存。

 context.Objects.MergeOption = MergeOption.NoTracking; var retrievedObj= context.Objects.Find(obj.Id); 

AppendOnly选项相反

只会追加新的(顶级唯一的)行。 这是默认行为。

这是您一直在努力的默认行为

您确定在所有查询中都出现问题吗? 在您的示例中,您使用了Find(),如果使用ToList()会怎么样? 问题不会发生,对吧?

出于测试目的,尝试使用Where方法而不是Find(),我相信你不会有问题……

如果上述理论为真,则在某种类型的存储库类中替换Find()by Where。 然后,您无需更改代码中的任何其他内容。

例如,在您的存储库类中:

 public YourClass Find(id) { //do not use Find here return context.FirstOrDefault(i => i.Id == id); //or Where(i => i.Id == id).FirstOrDefault(); } 

在您的业务逻辑中:

 var user = repository.Find(id); 

Find()方法文档https://msdn.microsoft.com/en-us/library/system.data.entity.dbset.find%28v=vs.113%29.aspx说:

“…如果上下文中存在具有给定主键值的实体,则会立即返回,而不向商店发出请求……”

所以,我认为问题是Find()。 使用存储库模式,替换Find by Where,是我现在可以想象的最简单的解决方法。 或者,您可以检查软删除是否已激活,然后选择您喜欢的方法,而不是替换。 你觉得怎么样?

一种更难的方法是创建一个inheritance自DbSet并覆盖Find()的类,这将太复杂。

编辑

为了帮助我们了解正在发生的事情,请创建一个控制台应用程序并记录数据库操作,如下所示:

 using (var context = new BlogContext()) { context.Database.Log = Console.Write; // Your code here... // Call your query twice, with and without softdelete } 

粘贴日志,然后我们将确定sql是否不正确或数据是否被缓存。

编辑2

好的…而不是在配置类的构造函数中添加拦截器,将其添加到上下文的构造函数中,如下所示:

 //the dbcontext class private IDbCommandTreeInterceptor softDeleteInterceptor; public DataContext() : base("YourConnection") { //add the interceptor softDeleteInterceptor = new SoftDeleteInterceptor() DbInterception.Add(softDeleteInterceptor); } 

然后,在上下文类中,创建一个删除拦截器的方法,如下所示:

 public void DisableSoftDelete() { DbInterception.Remove(softDeleteInterceptor); } 

当你想要禁用softdelete, context.DisableSoftDelete();时,调用上面的方法context.DisableSoftDelete();

要更新softdelete,可以覆盖SaveChanges方法,并且要创建filter,可以使用dbContext.Query() ,它将使用表达式生成器自动应用软删除filter。

要过滤软删除列,可以在DbContext中实现以下方法。

 public IQueryable Query(){ var ds = this.Set() as IQueryable; var entityType = typeof(T); if(!softDeleteSupported) return ds; ParameterExpression pe = Expression.Parameter(entityType); Expression compare = Expression.Equals( Expression.Property(pe, "SoftDeleted"), Expression.Constant(false)); Expression> filter = Expression.Lambda>(compare,pe); return ds.Where(filter); }