5

Sé que esto es posible en LINQ-to-SQL, y he visto fragmentos que me llevan a creer que es posible en EF. ¿Hay una extensión por ahí que puede hacer algo como esto:Código de EF Primero, ¿Eliminar lote de IQueryable <T>?

var peopleQuery = Context.People.Where(p => p.Name == "Jim"); 

peopleQuery.DeleteBatch(); 

Dónde DeleteBatch simplemente recoge además la peopleQuery y crea una única instrucción SQL para eliminar todos los registros apropiados, a continuación, ejecuta la consulta directamente en lugar de marcar todos aquellos entidades para su eliminación y tenerlas hacerlas una por una. Pensé que encontré algo así en el siguiente código, pero falla inmediatamente porque la instancia no se puede convertir en ObjectSet. ¿Alguien sabe cómo arreglar esto para trabajar con EF Code First? ¿O sabe de algún lugar que tenga un ejemplo de esto?

public static IQueryable<T> DeleteBatch<T>(this IQueryable<T> instance) where T : class 
{ 
    ObjectSet<T> query = instance as ObjectSet<T>; 
    ObjectContext context = query.Context; 

    string sqlClause = GetClause<T>(instance); 
    context.ExecuteStoreCommand("DELETE {0}", sqlClause); 

    return instance; 
} 

public static string GetClause<T>(this IQueryable<T> clause) where T : class 
{ 
    string snippet = "FROM [dbo].["; 

    string sql = ((ObjectQuery<T>)clause).ToTraceString(); 
    string sqlFirstPart = sql.Substring(sql.IndexOf(snippet)); 

    sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", ""); 
    sqlFirstPart = sqlFirstPart.Replace("[Extent1].", ""); 

    return sqlFirstPart; 
} 

Respuesta

5

Entidad marco no admite operaciones por lotes. Me gusta la forma en que el código resuelve el problema, pero incluso si hace exactamente lo que quiere (pero para ObjectContext API) es una solución incorrecta.

¿Por qué es una solución incorrecta?

Funciona solo en algunos casos. Definitivamente no funcionará en ninguna solución de mapeo avanzada donde la entidad se mapee a múltiples tablas (división de entidades, herencia de TPT). Estoy casi seguro de que puedes encontrar otras situaciones en las que no funcionará debido a la complejidad de la consulta.

Mantiene el contexto y la base de datos inconsistentes. Este es un problema de cualquier SQL ejecutado contra DB, pero en este caso el SQL está oculto y otro programador que use su código puede perderlo. Si borra cualquier registro cargado al mismo tiempo en la instancia de contexto, la entidad no se marcará como eliminada y se eliminará del contexto (a menos que agregue ese código a su método DeleteBatch; esto será especialmente complicado si los registros eliminados en realidad se mapean). a entidades múltiples (división de tabla)).

El problema más importante es la modificación de la consulta SQL generada por EF y las suposiciones que se realizan en esa consulta. Está esperando que EF nombre la primera tabla utilizada en la consulta como Extent1. Sí, realmente usa ese nombre ahora, pero es una implementación interna de EF. Puede cambiar en cualquier actualización menor de EF. Crear una lógica personalizada en torno a las partes internas de cualquier API se considera una mala práctica.

Como resultado, ya tiene que trabajar con la consulta en el nivel SQL para que pueda llamar directamente a la consulta SQL, ya que @mreyeros mostró y evitar riesgos en esta solución. Tendrá que lidiar con nombres reales de tablas y columnas, pero eso es algo que puede controlar (su mapeo puede definirlos).

Si no tenemos en cuenta estos riesgos tan significativa que puede hacer pequeños cambios en el código para que funcione en DbContext API:

public static class DbContextExtensions 
{ 
    public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : class 
    { 
     string sqlClause = GetClause<T>(query); 
     context.Database.ExecuteSqlCommand(String.Format("DELETE {0}", sqlClause)); 
    } 

    private static string GetClause<T>(IQueryable<T> clause) where T : class 
    { 
     string snippet = "FROM [dbo].["; 

     string sql = clause.ToString(); 
     string sqlFirstPart = sql.Substring(sql.IndexOf(snippet)); 

     sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", ""); 
     sqlFirstPart = sqlFirstPart.Replace("[Extent1].", ""); 

     return sqlFirstPart; 
    } 
} 

Ahora se llamará a eliminar por lotes de esta manera:

context.DeleteBatch(context.People.Where(p => p.Name == "Jim")); 
+0

Estoy de acuerdo con usted al 100% en todos sus puntos, pero es un riesgo aceptable en nuestro proyecto teniendo en cuenta el golpe de rendimiento serio que se tomó al hacerlo de manera oficial. ¡Gracias por la respuesta! – Ocelot20

1

No creo que las operaciones por lotes, como delete sean compatibles aún con EF. Puede ejecutar una consulta sin procesar:

context.Database.ExecuteSqlCommand("delete from dbo.tbl_Users where isActive = 0"); 
+0

Yo sé que no son compatibles actualmente, pero esperaba que pudiera construye ese comando de eliminación directamente desde la expresión como pude hacerlo en el pasado. – Ocelot20

1

En caso de que alguien más esté buscando esta funcionalidad, he usado algunos de los comentarios de Ladislav para mejorar su ejemplo. Como dijo, con la solución original, cuando llame al SaveChanges(), si el contexto ya estaba rastreando una de las entidades que eliminó, lo llamará su propia eliminación.Esto no modifica ningún registro, y EF lo considera un problema de concurrencia y arroja una excepción. El método a continuación es más lento que el original, ya que primero debe consultar los elementos para eliminar, pero no escribirá una única consulta de eliminación para cada entidad eliminada, que es el beneficio real del rendimiento. Separa a todas las entidades que fueron consultadas, por lo que si alguna de ellas ya fue rastreada, sabrá que ya no las eliminará.

public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : LcmpTableBase 
{ 
    IEnumerable<T> toDelete = query.ToList(); 

    context.Database.ExecuteSqlCommand(GetDeleteCommand(query)); 

    var toRemove = context.ChangeTracker.Entries<T>().Where(t => t.State == EntityState.Deleted).ToList(); 

    foreach (var t in toRemove) 
     t.State = EntityState.Detached; 
} 

También ha cambiado hasta esta parte para utilizar una expresión regular desde que pareció que había una cantidad indeterminada de espacios en blanco cerca de la porción FROM. También me dejó "[Extent1]" ahí porque la consulta DELETE escrito en la forma original no podría controlar las consultas con combinaciones internas:

public static string GetDeleteCommand<T>(this IQueryable<T> clause) where T : class 
{ 
    string sql = clause.ToString(); 

    Match match = Regex.Match(sql, @"FROM\s*\[dbo\].", RegexOptions.IgnoreCase); 

    return string.Format("DELETE [Extent1] {0}", sql.Substring(match.Index)); 
} 
Cuestiones relacionadas