Подтвердить что ты не робот

EF6 Отключить кэширование плана запроса с помощью перехватчика дерева команд

Я использую IDbCommandTreeInterceptor для реализации функциональности soft-delete. Внутри стандартного метода TreeCreated я проверяю, содержит ли данная команда запроса модели с атрибутом soft-delete. Если они это сделают, и пользователь попросил также получить мягкий удаленный объект --- я вызываю своего посетителя с мягким удалением с помощью querySoftDeleted= true. Это приведет к тому, что мой запрос вернет весь объект, те с true и те, у кого false значения в свойстве 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 решает, что этот запрос был таким же, как и раньше, и извлекал его из кеша. Полученный запрос не содержит фильтра и, таким образом, возвращает все объекты (soft-deleted и not). Кэш не очищается, когда контекст расположен.

Теперь вопрос заключается в том, есть ли способ, в идеале, пометить построенный DbCommand так, чтобы он не был кэширован. Это можно сделать? Или есть способ принудительно выполнить перекомпиляцию запроса?

Есть способы избежать кеширования, но я бы предпочел не менять каждый запрос в приложении, чтобы исправить это.

Подробнее о кешировании Query Plan можно найти здесь.

Изменить 1

Я использую новый контекст для каждого запроса - не должно быть проблем с кэшированием объектов.

Изменить 2

Вот журнал базы данных. Первый вызов - с soft-delete, а второй - без него. ... части идентичны, поэтому я исключил их из журнала. Вы можете видеть, что оба запроса идентичны. Сначала вызывается CreateTree, и приведенное дерево кэшируется так, что при выполнении дерево извлекается из кеша, и мой флаг soft-delete не применяется повторно, когда это должно быть.

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()
        }
4b9b3361

Ответ 1

Важно различать Query Plan Caching и Кэширование результатов:

Кэширование в инфраструктуре Entity

Кэширование плана запроса

При первом выполнении запроса он проходит через внутренний компилятор плана для перевода концептуального запроса в команду store (например, T-SQL, которая выполняется при запуске с SQL Server). Если кеширование плана запроса включено, при следующем выполнении запроса команда хранилища извлекается непосредственно из кеша плана запроса для выполнения, минуя компилятор плана.

Кэш плана запроса разделяется между экземплярами ObjectContext внутри тот же AppDomain. Вам не нужно удерживать объект ObjectContext экземпляр, чтобы извлечь выгоду из кэширования плана запроса.

Кэш запросов - это оптимизированный план инструкций SQL. Эти планы помогают делать запросы EF быстрее, чем "Холодные" запросы. Эти планы скрыты за пределами и конкретным контекстом.

Кэширование объектов:

По умолчанию, когда объект возвращается в результатах запроса, просто до того, как EF материализует его, ObjectContext будет проверять, является ли объект с тем же ключом уже загружен в ObjectStateManager. Если объект с теми же ключами уже присутствует, EF будет включать его в результатах запроса. Хотя EF все равно выдаст запрос против базы данных, это поведение может обойти большую часть стоимости материализуя объект несколько раз.

Другими словами, Object Caching - это мягкая форма кэширования результатов. Никакой другой вид 2-го уровня Cache доступен с Entity Framework, если вы специально его не включили. Кэширование второго уровня в платформе Entity Framework и Azure

AsNoTracking

Возвращает новый запрос, в котором возвращенные объекты не будут кэшироваться в DbContext или ObjectContext

Context.Set<Objects>().AsNoTracking();

Или вы можете отключить кэширование объектов для объекта, используя MergeOption NoTracking Опция:

Не будет изменять кеш.

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

В отличие от AppendOnly Option

Будет добавлено только новые (уникальные) строки. Это поведение по умолчанию.

это поведение по умолчанию, с которым вы боролись

Ответ 2

Вы уверены, что проблема возникает во всех запросах? В вашем примере вы использовали Find(), что, если вы используете ToList()? Проблема не происходит, не так ли?

В целях тестирования попробуйте использовать метод Where вместо Find(), я считаю, что у вас не будет проблем...

Если приведенная выше теория верна, замените Find() на 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, является самым простым обходным решением, которое я могу себе представить прямо сейчас. Или вместо замены вы можете проверить, активирован ли программный режим, а затем выбрать предпочтительный метод. Что вы думаете об этом?

Более сложным подходом является создание класса, который наследует от 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

Ok... вместо добавления перехватчика в конструкторе класса конфигурации добавьте его в конструктор контекста, например:

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

Вызвать метод выше, если вы хотите отключить программный файл, context.DisableSoftDelete();

Ответ 3

Чтобы обновить softdelete, вы можете переопределить метод 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);
}

Ответ 4

Я знаю, что этот вопрос был задан некоторое время назад, но так как ни один пост не был помечен как ответ, я поделюсь своим недавним опытом с этой проблемой. Как правильно объяснил Дэйв, "важно различать кеширование плана запросов и кеширование результатов". Здесь проблема кеширования плана запросов, потому что

Query Cache - это оптимизированный план инструкций SQL. Эти планы помогают выполнять запросы EF быстрее, чем "холодные" запросы. Эти планы кэшируются вне определенного контекста.

Таким образом, даже если вы создадите новый контекст, проблема останется. Мое решение этого очень простое. Вместо того чтобы полагаться только на Interceptor, мы можем добавить предложение Where в запрос в случае IfFetchSoftDeleted = true. При этом 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);
}