¿Cómo elimino varias filas en Entity Framework (sin foreach)

305

Estoy eliminando varios elementos de una tabla usando Entity Framework. No hay una clave externa / objeto primario, así que no puedo manejar esto con OnDeleteCascade.

En este momento estoy haciendo esto:

var widgets = context.Widgets
    .Where(w => w.WidgetId == widgetId);

foreach (Widget widget in widgets)
{
    context.Widgets.DeleteObject(widget);
}
context.SaveChanges();

Funciona pero el foreach me molesta. Estoy usando EF4 pero no quiero ejecutar SQL. Solo quiero asegurarme de que no me falta nada, esto es tan bueno como se pone, ¿verdad? Puedo abstraerlo con un método de extensión o ayudante, pero en algún lugar todavía vamos a hacer un foreach, ¿verdad?

Jon Galloway
fuente
1
Es posible que desee volver a visitar la respuesta aceptada.
Eric J.
1
Si quieres mantenerte activo, quizás quieras consultar mi respuesta aquí stackoverflow.com/a/35033286/274589
Adi

Respuestas:

49

Si no desea ejecutar SQL directamente, llamar a DeleteObject en un bucle es lo mejor que puede hacer hoy.

Sin embargo, puede ejecutar SQL y aún así hacerlo completamente general a través de un método de extensión, utilizando el enfoque que describo aquí .

Aunque esa respuesta fue para 3.5. Para 4.0 probablemente usaría la nueva API ExecuteStoreCommand debajo del capó, en lugar de desplegarme en StoreConnection.

Alex James
fuente
ExecuteStoreCommand no es una forma adecuada. DeleteAllSubmit funciona en linq to sql pero no en el marco de la entidad. Quiero la misma opción en el marco de la entidad.
Hiral
653

EntityFramework 6 ha hecho esto un poco más fácil con .RemoveRange().

Ejemplo:

db.People.RemoveRange(db.People.Where(x => x.State == "CA"));
db.SaveChanges();
Kyle
fuente
31
Eso es exactamente lo que necesitamos ... ¡Excepto cuando lo uso en un rango lo suficientemente grande, obtengo una excepción de falta de memoria! Pensé que el objetivo de RemoveRange era pasar el procesamiento a la base de datos, pero aparentemente no.
Samer Adra
¡esto es MUCHO MÁS rápido que establecer el estado Eliminado en cada entidad!
Jerther
54
Seguro que esta respuesta es más fácil, pero en cuanto al rendimiento, puede que no sea excelente. ¿Por qué? lo que este doet exatly es lo mismo que eliminarlo en foreach loop, primero recupera todas las filas y luego eliminar es una por una, solo la ganancia es para guardar "DetectChanges se llamará una vez antes de eliminar cualquier entidad y no se volverá a llamar" rest es lo mismo, intente usar la herramienta para ver sql generado.
Anshul Nigam
66
Para un rango lo suficientemente grande, intente algo como .Take (10000) y realice un bucle hasta RemoveRange (...). Count () == 0.
Eric J.
20
El problema es que el parámetro de entrada RemoveRange es un IEnumerable, por lo que para realizar la eliminación enumera todas las entidades y ejecuta 1 consulta DELETE por entidad.
bubi
74

esto es tan bueno como se pone, ¿verdad? Puedo abstraerlo con un método de extensión o ayudante, pero en algún lugar todavía vamos a hacer un foreach, ¿verdad?

Bueno, sí, excepto que puedes convertirlo en dos líneas:

context.Widgets.Where(w => w.WidgetId == widgetId)
               .ToList().ForEach(context.Widgets.DeleteObject);
context.SaveChanges();
Klaus Byskov Pedersen
fuente
76
Estás haciendo una ToList () que anula el propósito. ¿Cómo es eso diferente de la solución original?
Lahsrah
3
Tengo problemas ya que solo tengo el método Remove en el objeto de contexto.
Pnct
2
Definitivamente, esta no es una solución adecuada cuando se esperan un millón de filas (o incluso unos pocos cientos). Sin embargo, si sabemos con certeza que solo habrá unas pocas filas, esta solución es ordenada y funciona perfectamente bien. Sí, implicaría algunos viajes de ida y vuelta a la base de datos, pero en mi opinión, la abstracción perdida involucrada en llamar a SQL directamente supera los beneficios.
Yogster
Entity Framework, como su nombre lo indica, funciona mejor con datos a nivel de entidad. Las operaciones de datos masivos se manejan mejor con buenos procedimientos almacenados. En cuanto al rendimiento, son de lejos las mejores opciones y superarán cualquier lógica EF que requiera un bucle.
Paceman
72
using (var context = new DatabaseEntities())
{
    context.ExecuteStoreCommand("DELETE FROM YOURTABLE WHERE CustomerID = {0}", customerId);
}
Vlad Bezden
fuente
Pero, ¿cómo puedes hacer esto con una lista de identificadores? Esta solución no maneja muy bien las "listas".
JesseNewman19
11
@ JesseNewman19 Si ya tiene una lista de ID, use a WHERE IN ({0}), y entonces el segundo argumento debería ser String.Join(",", idList).
Langdon
@Langdon que no funcionará, porque enviará el comando a sql así: WHERE IN ("1, 2, 3"). La base de datos arroja un error porque le pasó una cadena en lugar de una lista de enteros.
JesseNewman19
Deseo generar una declaración como esa con LINQ. Lo más parecido que encontré fue una lib. EntityFramework.Extended
Jaider
Si está utilizando String.Join, es posible que necesite usar string.Formaty pasar la cadena SQL ya formada al comando. Mientras su lista solo tenga números enteros, no hay riesgo de ataque de inyección. Verifique esta pregunta: ¿cómo puedo pasar una matriz a un comando de ejecutar tienda?
Andrew
50

Sé que es bastante tarde, pero en caso de que alguien necesite una solución simple, lo bueno es que también puede agregar la cláusula where con ella:

public static void DeleteWhere<T>(this DbContext db, Expression<Func<T, bool>> filter) where T : class
{
    string selectSql = db.Set<T>().Where(filter).ToString();
    string fromWhere = selectSql.Substring(selectSql.IndexOf("FROM"));
    string deleteSql = "DELETE [Extent1] " + fromWhere;
    db.Database.ExecuteSqlCommand(deleteSql);
}

Nota: recién probado con MSSQL2008.

Actualizar:

La solución anterior no funcionará cuando EF genere una instrucción sql con parámetros , así que aquí está la actualización para EF5 :

public static void DeleteWhere<T>(this DbContext db, Expression<Func<T, bool>> filter) where T : class
{
    var query = db.Set<T>().Where(filter);

    string selectSql = query.ToString();
    string deleteSql = "DELETE [Extent1] " + selectSql.Substring(selectSql.IndexOf("FROM"));

    var internalQuery = query.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(field => field.Name == "_internalQuery").Select(field => field.GetValue(query)).First();
    var objectQuery = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(field => field.Name == "_objectQuery").Select(field => field.GetValue(internalQuery)).First() as ObjectQuery;
    var parameters = objectQuery.Parameters.Select(p => new SqlParameter(p.Name, p.Value)).ToArray();

    db.Database.ExecuteSqlCommand(deleteSql, parameters);
}

Requiere un poco de reflexión pero funciona bien.

Thanh Nguyen
fuente
¿Qué es DbContext? ¿Asumo su contexto de marco de entidad autogenerado? No tengo un método llamado Set <T>.
Stealth Rabbi
@Stealth: Sí, es su contexto de datos EF, utilizo el código primero pero el contexto generado automáticamente debería ser el mismo. Perdón por la declaración mal escrita, debe establecerse <T> () (mi empresa restringe el acceso a Internet, no pude pegar el código, tuve que escribir a mano, así que ...), códigos actualizados :)
Thanh Nguyen
3
¡Esta es la única respuesta que realmente responde la pregunta! Cualquier otra respuesta elimina cada elemento individual, uno a la vez, increíble.
Rocklan
Esto se siente como la respuesta más correcta. Permite la eliminación de una manera muy genérica, y descarga correctamente el trabajo a la base de datos y no a C #.
JesseNewman19
1
Para todos los programadores menos técnicos, quería desarrollar un poco más sobre cómo implementar esta solución excelente y genérica, ¡porque me habría ahorrado unos minutos de tiempo! Continúa en el próximo comentario ...
jdnew18
30

Para cualquiera que use EF5, se puede usar la siguiente biblioteca de extensiones: https://github.com/loresoft/EntityFramework.Extended

context.Widgets.Delete(w => w.WidgetId == widgetId);
Marcelo Mason
fuente
3
Tiene problemas de rendimiento en tablas grandes, no utilizables en mi situación.
Tomás
@Tomas, ¿qué tipo de presentación emitida notaste? ¿Qué tan grave fue el problema y qué tan grande fue la tabla? ¿Alguien más puede confirmar eso?
Anestis Kivranoglou
Es realmente rápido en comparación con las alternativas disponibles
Jaider
No puedo ver la Delete()función en mis entidades en EF6.
dotNET
context.Widgets.Where(w => w.WidgetId == widgetId).Delete();es la forma más nueva con EntityFramework
Peter Kerr
11

Todavía parece una locura tener que retirar algo del servidor solo para eliminarlo, pero al menos recuperar solo las ID es mucho más ágil que eliminar las entidades completas:

var ids = from w in context.Widgets where w.WidgetId == widgetId select w.Id;
context.Widgets.RemoveRange(from id in ids.AsEnumerable() select new Widget { Id = id });
Edward Brey
fuente
Tenga cuidado: esto puede fallar la validación de la entidad de Entity Framework porque sus Widgetobjetos de código auxiliar solo tienen una Idpropiedad inicializada . La forma de evitar esto es usar context.Configuration.ValidateOnSaveEnabled = false(al menos en EF6). Esto deshabilita la validación propia de Entity Framework, pero todavía realiza la validación propia de la base de datos, por supuesto.
Sammy S.
@SammyS. No he experimentado eso, así que no puedo hablar de los detalles, pero parece extraño que EF se moleste con la validación cuando se elimina la fila de todos modos.
Edward Brey
Estás absolutamente en lo correcto. Confundí el deletecon una solución similar para updatelas entidades ing sin cargarlas.
Sammy S.
10

EF 6.1

public void DeleteWhere<TEntity>(Expression<Func<TEntity, bool>> predicate = null) 
where TEntity : class
{
    var dbSet = context.Set<TEntity>();
    if (predicate != null)
        dbSet.RemoveRange(dbSet.Where(predicate));
    else
        dbSet.RemoveRange(dbSet);

    context.SaveChanges();
} 

Uso:

// Delete where condition is met.
DeleteWhere<MyEntity>(d => d.Name == "Something");

Or:

// delete all from entity
DeleteWhere<MyEntity>();
mnsr
fuente
77
Esto es efectivamente lo mismo que db.People.RemoveRange (db.People.Where (x => x.State == "CA")); db.SaveChanges (); Entonces no hay ganancia de rendimiento.
ReinierDG
4

Para EF 4.1,

var objectContext = (myEntities as IObjectContextAdapter).ObjectContext;
objectContext.ExecuteStoreCommand("delete from [myTable];");
Amit Pawar
fuente
1
Esto funciona, pero el objetivo de usar Entity Framework es tener una forma orientada a objetos para interactuar con la base de datos. Esto solo está ejecutando directamente la consulta SQL.
Arturo Torres Sánchez
4

Puede usar bibliotecas de extensiones para hacer eso como EntityFramework.Extended o Z.EntityFramework.Plus.EF6, están disponibles para EF 5, 6 o Core. Estas bibliotecas tienen un gran rendimiento cuando tiene que eliminar o actualizar y usan LINQ. Ejemplo para eliminar ( fuente más ):

ctx.Users.Where(x => x.LastLoginDate < DateTime.Now.AddYears(-2)) .Delete();

o ( fuente extendida )

context.Users.Where(u => u.FirstName == "firstname") .Delete();

Estos usan sentencias SQL nativas, por lo que el rendimiento es excelente.

UUHHIVS
fuente
Pague 600 $ + por el generador de operaciones sql a granel. ¿Seriamente?
nicolay.anykienko
@ nicolay.anykienko Cuando lo usé, esta biblioteca era gratuita, hay otras operaciones en las que tienes que pagar, no, no sé si tienes que pagar
UUHHIVS
3

La forma más rápida de eliminar es usar un procedimiento almacenado. Prefiero procedimientos almacenados en un proyecto de base de datos sobre SQL dinámico porque los cambios de nombre se manejarán correctamente y tendrán errores de compilación. El SQL dinámico podría referirse a tablas que se han eliminado / renombrado causando errores de tiempo de ejecución.

En este ejemplo, tengo dos tablas List y ListItems. Necesito una forma rápida de eliminar todos los elementos de lista de una lista determinada.

CREATE TABLE [act].[Lists]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY, 
    [Name] NVARCHAR(50) NOT NULL
)
GO
CREATE UNIQUE INDEX [IU_Name] ON [act].[Lists] ([Name])
GO
CREATE TABLE [act].[ListItems]
(
    [Id] INT NOT NULL IDENTITY, 
    [ListId] INT NOT NULL, 
    [Item] NVARCHAR(100) NOT NULL, 
    CONSTRAINT PK_ListItems_Id PRIMARY KEY NONCLUSTERED (Id),
    CONSTRAINT [FK_ListItems_Lists] FOREIGN KEY ([ListId]) REFERENCES [act].[Lists]([Id]) ON DELETE CASCADE
)
go
CREATE UNIQUE CLUSTERED INDEX IX_ListItems_Item 
ON [act].[ListItems] ([ListId], [Item]); 
GO

CREATE PROCEDURE [act].[DeleteAllItemsInList]
    @listId int
AS
    DELETE FROM act.ListItems where ListId = @listId
RETURN 0

Ahora la parte interesante de eliminar los elementos y actualizar el marco de Entity usando una extensión.

public static class ListExtension
{
    public static void DeleteAllListItems(this List list, ActDbContext db)
    {
        if (list.Id > 0)
        {
            var listIdParameter = new SqlParameter("ListId", list.Id);
            db.Database.ExecuteSqlCommand("[act].[DeleteAllItemsInList] @ListId", listIdParameter);
        }
        foreach (var listItem in list.ListItems.ToList())
        {
            db.Entry(listItem).State = EntityState.Detached;
        }
    }
}

El código principal ahora puede usarlo es como

[TestMethod]
public void DeleteAllItemsInListAfterSavingToDatabase()
{
    using (var db = new ActDbContext())
    {
        var listName = "TestList";
        // Clean up
        var listInDb = db.Lists.Where(r => r.Name == listName).FirstOrDefault();
        if (listInDb != null)
        {
            db.Lists.Remove(listInDb);
            db.SaveChanges();
        }

        // Test
        var list = new List() { Name = listName };
        list.ListItems.Add(new ListItem() { Item = "Item 1" });
        list.ListItems.Add(new ListItem() { Item = "Item 2" });
        db.Lists.Add(list);
        db.SaveChanges();
        listInDb = db.Lists.Find(list.Id);
        Assert.AreEqual(2, list.ListItems.Count);
        list.DeleteAllListItems(db);
        db.SaveChanges();
        listInDb = db.Lists.Find(list.Id);
        Assert.AreEqual(0, list.ListItems.Count);
    }
}
Xavier John
fuente
Gracias por un buen ejemplo de cómo usar un Procedimiento almacenado y luego implementarlo como una extensión, con el código de Uso.
Glenn Garson
3

Si desea eliminar todas las filas de una tabla, puede ejecutar el comando sql

using (var context = new DataDb())
{
     context.Database.ExecuteSqlCommand("TRUNCATE TABLE [TableName]");
}

TRUNCATE TABLE (Transact-SQL) Elimina todas las filas de una tabla sin registrar las eliminaciones de filas individuales. TRUNCATE TABLE es similar a la instrucción DELETE sin cláusula WHERE; sin embargo, TRUNCATE TABLE es más rápido y utiliza menos recursos del sistema y del registro de transacciones.

Amir
fuente
3
También debe mencionar que no puede ejecutarse truncate tableen tablas a las que hace referencia una restricción FOREIGN KEY. (Puede truncar una tabla que tiene una clave externa que hace referencia a sí misma). Documentación de MSDN
banda ancha
2

UUHHIVS's es una forma muy elegante y rápida de eliminar lotes, pero debe usarse con cuidado:

  • generación automática de transacción: sus consultas serán abarcadas por una transacción
  • independencia del contexto de la base de datos: su ejecución no tiene nada que ver con context.SaveChanges()

Estos problemas pueden evitarse tomando el control de la transacción. El siguiente código ilustra cómo eliminar por lotes e insertar en masa de manera transaccional:

var repo = DataAccess.EntityRepository;
var existingData = repo.All.Where(x => x.ParentId == parentId);  

TransactionScope scope = null;
try
{
    // this starts the outer transaction 
    using (scope = new TransactionScope(TransactionScopeOption.Required))
    {
        // this starts and commits an inner transaction
        existingData.Delete();

        // var toInsert = ... 

        // this relies on EntityFramework.BulkInsert library
        repo.BulkInsert(toInsert);

        // any other context changes can be performed

        // this starts and commit an inner transaction
        DataAccess.SaveChanges();

        // this commit the outer transaction
        scope.Complete();
    }
}
catch (Exception exc)
{
    // this also rollbacks any pending transactions
    scope?.Dispose();
}
Alexei
fuente
2

Entity Framework Core

3.1 3.0 2.2 2.1 2.0 1.1 1.0

using (YourContext context = new YourContext ())
{
    var widgets = context.Widgets.Where(w => w.WidgetId == widgetId);
    context.Widgets.RemoveRange(widgets);
    context.SaveChanges();
}

Resumen :

Elimina la colección dada de entidades del contexto subyacente al conjunto con cada entidad que se coloca en el estado Suprimido de modo que se eliminará de la base de datos cuando se llame a SaveChanges.

Observaciones :

Tenga en cuenta que si System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled se establece en true (que es el valor predeterminado), se llamará a DetectChanges una vez antes de eliminar cualquier entidad y no se volverá a llamar. Esto significa que, en algunas situaciones, RemoveRange puede funcionar significativamente mejor que llamar a Remove varias veces. Tenga en cuenta que si existe alguna entidad en el contexto en el estado Agregado, entonces este método hará que se separe del contexto. Esto se debe a que se supone que una entidad agregada no existe en la base de datos, de modo que intentar eliminarla no tiene sentido.

Nguyen Van Thanh
fuente
1

Puede ejecutar consultas sql directamente de la siguiente manera:

    private int DeleteData()
{
    using (var ctx = new MyEntities(this.ConnectionString))
    {
        if (ctx != null)
        {

            //Delete command
            return ctx.ExecuteStoreCommand("DELETE FROM ALARM WHERE AlarmID > 100");

        }
    }
    return 0;
}

Para seleccionar podemos usar

using (var context = new MyContext()) 
{ 
    var blogs = context.MyTable.SqlQuery("SELECT * FROM dbo.MyTable").ToList(); 
}
Abhishek Sharma
fuente
Dado que EF no admite correctamente el mapeo de las condiciones de eliminación, esta es probablemente su mejor opción para hacer el trabajo.
Tony O'Hagan
1

También puede usar el método DeleteAllOnSubmit () pasando sus resultados en una lista genérica en lugar de en var. De esta manera, su foreach se reduce a una línea de código:

List<Widgets> widgetList = context.Widgets
              .Where(w => w.WidgetId == widgetId).ToList<Widgets>();

context.Widgets.DeleteAllOnSubmit(widgetList);

context.SubmitChanges();

Sin embargo, probablemente todavía use un bucle interno.

Hugo Nava Kopp
fuente
3
Parece que estás malentendido lo que vares.
freedomn-m
1

La respuesta de Thanh funcionó mejor para mí. Eliminé todos mis registros en un solo viaje de servidor. Me costó realmente llamar al método de extensión, así que pensé en compartir el mío (EF 6):

Agregué el método de extensión a una clase auxiliar en mi proyecto MVC y cambié el nombre a "RemoveWhere". Inyecto un dbContext en mis controladores, pero también podría hacer un using.

// make a list of items to delete or just use conditionals against fields
var idsToFilter = dbContext.Products
    .Where(p => p.IsExpired)
    .Select(p => p.ProductId)
    .ToList();

// build the expression
Expression<Func<Product, bool>> deleteList = 
    (a) => idsToFilter.Contains(a.ProductId);

// Run the extension method (make sure you have `using namespace` at the top)
dbContext.RemoveWhere(deleteList);

Esto generó una sola declaración de eliminación para el grupo.

Steve Greene
fuente
0

EF 6. =>

var assignmentAddedContent = dbHazirBot.tbl_AssignmentAddedContent.Where(a =>
a.HazirBot_CategoryAssignmentID == categoryAssignment.HazirBot_CategoryAssignmentID);
dbHazirBot.tbl_AssignmentAddedContent.RemoveRange(assignmentAddedContent);
dbHazirBot.SaveChanges();
Erçin Dedeoğlu
fuente
0

Mejor: in EF6 => .RemoveRange()

Ejemplo:

db.Table.RemoveRange(db.Table.Where(x => Field == "Something"));
maXXis
fuente
14
¿Cómo es esto diferente de la respuesta de Kyle?
-1

Vea la respuesta 'bit de código favorito' que funciona

Así es como lo usé:

     // Delete all rows from the WebLog table via the EF database context object
    // using a where clause that returns an IEnumerable typed list WebLog class 
    public IEnumerable<WebLog> DeleteAllWebLogEntries()
    {
        IEnumerable<WebLog> myEntities = context.WebLog.Where(e => e.WebLog_ID > 0);
        context.WebLog.RemoveRange(myEntities);
        context.SaveChanges();

        return myEntities;
    }
Brian Quinn
fuente
1
¿En qué se diferencia su respuesta de la respuesta del usuario 1308743 ?
Sergey Berezovskiy
Simplemente estaba compartiendo un ejemplo de trabajo. Lo que sea que pueda hacer para devolver la ayuda que obtengo aquí.
Brian Quinn
-3

En EF 6.2 esto funciona perfectamente, enviando la eliminación directamente a la base de datos sin cargar primero las entidades:

context.Widgets.Where(predicate).Delete();

Con un predicado fijo es bastante sencillo:

context.Widgets.Where(w => w.WidgetId == widgetId).Delete();

Y si necesita un predicado dinámico, eche un vistazo a LINQKit (paquete Nuget disponible), algo como esto funciona bien en mi caso:

Expression<Func<Widget, bool>> predicate = PredicateBuilder.New<Widget>(x => x.UserID == userID);
if (somePropertyValue != null)
{
    predicate = predicate.And(w => w.SomeProperty == somePropertyValue);
}
context.Widgets.Where(predicate).Delete();
Vladimir
fuente
1
Con EF 6.2 sin procesar esto no es posible. Tal vez estás usando Z.EntityFramework.Pluso algo similar? ( entityframework.net/batch-delete )
Sammy S.
El primero es EF 6.2 sin procesar y funciona find. El segundo es, como mencioné, usar LINQKit.
Vladimir
1
Hmm, no puedo encontrar este método. ¿Podría verificar en qué clase y en qué espacio de nombres reside este método?
Sammy S.
Tercero que (el Delete()método es inherentemente inexistente).
Suma ninguno