c# - EF6 Deshabilitar el caché del plan de consultas con el interceptor del árbol de comandos
entity-framework caching (3)
Estoy usando IDbCommandTreeInterceptor
para implementar la funcionalidad de eliminación suave. Dentro del método TreeCreated
estándar, compruebo si el comando de consulta dado contiene modelos con atributo de eliminación suave. Si lo hacen, y el usuario también solicitó recuperar el objeto eliminado suave --- Llamo a mi visitante de eliminación suave con querySoftDeleted
= true
. Esto hará que mi consulta devuelva todos los objetos, aquellos con true
y aquellos con valores false
en la propiedad 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.
}
}
El problema surge cuando trato de recuperar todos los objetos (también eliminados) y luego con el mismo objeto de consulta posterior que no se eliminan solamente. Algo como esto:
context.ShouldFetchSoftDeleted = true;
var retrievedObj= context.Objects.Find(obj.Id);
Y luego en una nueva instancia de contexto (no en el mismo contexto)
var retrievedObj= context.Objects.Find(obj.Id);
La segunda vez, ShouldFetchSoftDeleted
se establece en falso, todo está bien, pero EF decide que esta consulta era igual a la anterior y la recupera del caché. La consulta recuperada no contiene filtro y, por lo tanto, devuelve todos los objetos (borrados de forma suave y no). El caché no se borra cuando se elimina el contexto.
Ahora la pregunta es si hay una forma, idealmente, de marcar DbCommand
construido para que no se DbCommand
caché. Se puede hacer esto? ¿O hay una manera de forzar la recompilación de consultas?
Hay formas de evitar el almacenamiento en caché, pero preferiría no tener que cambiar cada consulta en la aplicación solo para solucionar este problema.
Más información sobre el Caché del Plan de Consulta se puede encontrar here .
Editar 1
Estoy usando un nuevo contexto para cada solicitud: el almacenamiento en caché de objetos no debería ser el problema.
Editar 2
Aquí está el registro de la base de datos. La primera llamada es con eliminación suave y la segunda es w / o. ...
partes son idénticas, así que las excluí del registro. Puedes ver que ambas peticiones son idénticas. Primero se llama CreateTree
y el árbol de resultados se almacena en la memoria caché, de modo que cuando se ejecuta, el árbol se recupera de la memoria caché y mi indicador de eliminación suave no se vuelve a aplicar cuando debería estarlo.
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.
Como ya he dicho, ejecuté cada solicitud en su propio contexto de la siguiente manera:
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()
}
¿Estás seguro de que tu problema pasa en todas las consultas? En tu ejemplo, has usado el Find (), ¿qué pasa si usas ToList ()? El problema no pasa, ¿verdad?
Para propósitos de prueba, intente usar el método Where en lugar de Find (), creo que no tendrá problemas ...
Si la teoría anterior es cierta, reemplace Find () por Where dentro de algún tipo de clase de repositorio. Entonces no necesitas cambiar nada más en tu código.
Por ejemplo, en su clase de repositorio:
public YourClass Find(id)
{
//do not use Find here
return context.FirstOrDefault(i => i.Id == id); //or Where(i => i.Id == id).FirstOrDefault();
}
En su lógica de negocio:
var user = repository.Find(id);
La documentación del método Find () https://msdn.microsoft.com/en-us/library/system.data.entity.dbset.find%28v=vs.113%29.aspx dice:
"... si una entidad con los valores de clave primaria dados existe en el contexto, entonces se devuelve inmediatamente sin hacer una solicitud a la tienda ..."
Por lo tanto, creo que el problema es el hallazgo (). Usar un patrón de repositorio, reemplazar Buscar por dónde, es la solución más fácil que puedo imaginar en este momento. O bien, en lugar de reemplazar, puedes verificar si el softdelete está activado y luego elegir tu método preferido. ¿Qué piensas sobre eso?
Un enfoque más difícil es crear una clase que herede de DbSet y anule el Find (), que será demasiado complicado.
EDITAR
Para ayudarnos a ver lo que está sucediendo, cree una aplicación de consola y registre la operación de la base de datos, como esto:
using (var context = new BlogContext())
{
context.Database.Log = Console.Write;
// Your code here...
// Call your query twice, with and without softdelete
}
Pegue el registro, luego veremos con seguridad si el sql es incorrecto o si los datos se almacenan en caché.
Editar 2
Ok ... en lugar de agregar el interceptor en el constructor de la clase de configuración, agréguelo en el constructor del contexto, así:
//the dbcontext class
private IDbCommandTreeInterceptor softDeleteInterceptor;
public DataContext()
: base("YourConnection")
{
//add the interceptor
softDeleteInterceptor = new SoftDeleteInterceptor()
DbInterception.Add(softDeleteInterceptor);
}
Luego, dentro de su clase de contexto, cree un método que elimine el interceptor, como este:
public void DisableSoftDelete()
{
DbInterception.Remove(softDeleteInterceptor);
}
Llame al método anterior cuando desee desactivar el softdelete, context.DisableSoftDelete();
Es importante distinguir entre el caché del plan de consultas y el caché de resultados :
Consulta del plan de caché
La primera vez que se ejecuta una consulta, pasa por el compilador del plan interno para traducir la consulta conceptual en el comando de la tienda (por ejemplo, el T-SQL que se ejecuta cuando se ejecuta contra SQL Server). Si el almacenamiento en caché del plan de consulta está habilitado, la próxima vez que se ejecute la consulta, el comando de almacenamiento se recuperará directamente de la caché del plan de consulta para su ejecución, omitiendo el compilador del plan.
El caché del plan de consulta se comparte entre las instancias de ObjectContext dentro del mismo dominio de aplicación. No es necesario mantener una instancia de ObjectContext para beneficiarse del almacenamiento en caché del plan de consulta.
Un Caché de consultas es un plan de instrucciones SQL optimizado. Estos planes ayudan a que las consultas de EF sean más rápidas que las consultas "Frías". Estos planes se almacenan en caché más allá de un contexto particular.
Almacenamiento en caché de objetos:
De forma predeterminada, cuando se devuelve una entidad en los resultados de una consulta, justo antes de que EF la materialice, el ObjectContext comprobará si una entidad con la misma clave ya se ha cargado en su ObjectStateManager. Si una entidad con las mismas claves ya está presente, EF la incluirá en los resultados de la consulta. Aunque EF seguirá emitiendo la consulta en la base de datos, este comportamiento puede evitar gran parte del costo de materializar la entidad varias veces.
En otras palabras, el almacenamiento en caché de objetos es una forma suave de almacenamiento en caché de resultados. Ningún otro tipo de caché de segundo nivel está disponible con Entity Framework a menos que lo incluya específicamente. Caché de segundo nivel en Entity Framework y Azure
Devuelve una nueva consulta donde las entidades devueltas no se almacenarán en caché en DbContext o ObjectContext
Context.Set<Objects>().AsNoTracking();
O puede deshabilitar el almacenamiento en caché de objetos para una entidad usando la opción MergeOption NoTracking
:
No modificará el caché.
context.Objects.MergeOption = MergeOption.NoTracking;
var retrievedObj= context.Objects.Find(obj.Id);
A diferencia de la Opción de AppendOnly
Solo agregará filas nuevas (de nivel superior-únicas). Este es el comportamiento predeterminado.
Este es el comportamiento predeterminado con el que has estado luchando.
Para actualizar softdelete, puede anular el método SaveChanges y, para crear un filtro, puede usar dbContext.Query<T>()
que aplicará el filtro de eliminación suave automáticamente usando el generador de expresiones.
Para filtrar su columna de eliminación suave, puede implementar el siguiente método, en su 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);
}