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

时间:2015-05-11 17:23:49

标签: c# entity-framework caching

我使用IDbCommandTreeInterceptor来实现软删除功能。在标准TreeCreated方法内部,我检查给定查询命令是否包含具有软删除属性的模型。如果他们这样做并且用户也要求获取软删除的对象 - 我使用querySoftDeleted = true来呼叫我的软删除访问者。这将使我的查询返回所有对象,truefalse属性IsDeleted值的对象。

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决定此查询与之前的查询相同,并从缓存中检索它。检索到的查询不包含过滤器,因此返回所有对象(软删除而不是)。处理上下文时不会清除缓存。

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

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

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

修改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()
        }

4 个答案:

答案 0 :(得分:8)

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

<强> Caching in the Entity Framework

查询计划缓存

  

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

     

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

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

对象缓存:

  

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

换句话说,对象缓存是结果缓存的一种软形式。除非您明确包含,否则实体框架不提供其他类型的二级缓存。 Second-Level Caching in the Entity Framework and Azure

AsNoTracking

  

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

$ bash sqlfinddrop.sh <dat/sql.dat

 The following tables are missing from DROP tables:

   rfvbgt_ttuop_3

或者您可以使用MergeOption Context.Set<Objects>().AsNoTracking(); 禁用实体的对象缓存选项:

  

不会修改缓存。

NoTracking

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

相对
  

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

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

答案 1 :(得分:2)

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

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

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

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

get_tip() takes exactly 2 arguments (1 given)

在您的业务逻辑中:

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

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

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

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

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

修改

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

var user = repository.Find(id);

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

编辑2

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

using (var context = new BlogContext()) 
{ 
    context.Database.Log = Console.Write; 

    // Your code here... 
    // Call your query twice, with and without softdelete
}

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

//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); }

,请调用上述方法

答案 2 :(得分:0)

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

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

public IQueryable<T> Query<T>(){

   var ds = this.Set<T>() as IQueryable<T>;

   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<Func<T,bool>> filter = 
       Expression.Lambda<Func<T,bool>>(compare,pe);

   return ds.Where(filter);
}

答案 3 :(得分:0)

我知道这个问题是在一段时间之前提出的,但是由于没有文章被标记为答案,因此我分享了我最近在该问题上的经验。正如Dave正确解释的那样,“区分查询计划缓存和结果缓存很重要”。这里的问题是查询计划缓存,因为

  

查询缓存是一种优化的SQL指令计划。这些计划有助于使EF查询比“冷”查询更快。 这些计划被缓存在特定的上下文之外。

因此,即使您创建新的上下文,问题仍然存在,因为拦截器未应用于“查询计划缓存”查询。我对此的解决方法非常简单。在ShouldFetchSoftDeleted = true的情况下,我们可以在查询中添加Where子句,而不是仅依赖于Interceptor。这样,EF使用了另一个查询,并且不会重复使用错误地缓存的查询。现在将调用拦截器,但是ShouldFetchSoftDeleted = true阻止您的QueryVisitor应用IsDeleted过滤器。

我使用的是存储库模式,但我认为概念很明确。

public override IQueryable<TEntity> Find<TEntity>()
{
   var query = GetRepository<TEntity>().Find();

   if (!ShouldFetchSoftDeleted)
   {
     return query; // interceptor handles soft delete
   }

   query = GetRepository<TEntity>().Find();
   return Where<IDeletedInfo, TEntity>(query, x => x.IsDeleted == false || x.IsDeleted);
}