Come posso eliminare più righe in Entity Framework (senza foreach)


305

Sto eliminando diversi elementi da una tabella utilizzando Entity Framework. Non esiste una chiave esterna / oggetto padre, quindi non posso gestirlo con OnDeleteCascade.

In questo momento sto facendo questo:

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

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

Funziona ma la foreach mi dà fastidio. Sto usando EF4 ma non voglio eseguire SQL. Voglio solo assicurarmi che non mi manchi nulla: va bene così, vero? Posso astrarlo con un metodo di estensione o un aiuto, ma da qualche parte stiamo ancora facendo una foreach, giusto?


1
Potresti voler rivisitare la risposta accettata.
Eric J.

1
Se vuoi rimanere performante forse potresti voler controllare la mia risposta qui stackoverflow.com/a/35033286/274589
Adi

Risposte:


49

Se non vuoi eseguire SQL direttamente chiamando DeleteObject in un ciclo è il meglio che puoi fare oggi.

Tuttavia è possibile eseguire SQL e renderlo ancora completamente generico tramite un metodo di estensione, usando l'approccio che descrivo qui .

Sebbene quella risposta fosse per 3.5. Per 4.0 probabilmente userò la nuova API ExecuteStoreCommand sotto il cofano, invece di passare a StoreConnection.


ExecuteStoreCommand non è un modo corretto.DeleteAllSubmit funziona in linq to sql ma non nel framework di entità. Voglio la stessa opzione nel quadro delle entità.
Hiral,

653

EntityFramework 6 ha reso tutto più semplice con .RemoveRange().

Esempio:

db.People.RemoveRange(db.People.Where(x => x.State == "CA"));
db.SaveChanges();

31
Questo è esattamente ciò di cui abbiamo bisogno ... Tranne quando lo uso su un intervallo sufficientemente ampio, ottengo un'eccezione di memoria insufficiente! Pensavo che il punto centrale di RemoveRange fosse passare l'elaborazione al database, ma apparentemente no.
Samer Adra,

questo è WAAAYYYY più veloce dell'impostazione dello stato Eliminato su ogni entità!
Jerther,

54
Sicuramente questa risposta è più semplice, ma per quanto riguarda le prestazioni potrebbe non essere eccezionale. Perché? ciò che questa doet esattamente è uguale all'eliminazione nel ciclo foreach, prima recupera tutte le righe e quindi elimina una per una, solo il guadagno è per il salvataggio "DetectChanges verrà chiamato una volta prima di eliminare qualsiasi entità e non verrà richiamato" resto è lo stesso, prova a usare lo strumento per vedere sql generato.
Anshul Nigam,

6
Per un intervallo sufficientemente ampio, prova qualcosa del tipo. Prendi (10000) e esegui il ciclo fino a Rimuovi intervallo (...). Count () == 0.
Eric J.

20
Il problema è che il parametro di input RemoveRange è un IEnumerable, quindi per eseguire la cancellazione enumera tutte le entità ed esegue 1 query DELETE per entità.
bubi,

74

va bene così, vero? Posso astrarlo con un metodo di estensione o un aiuto, ma da qualche parte stiamo ancora facendo una foreach, giusto?

Bene, sì, tranne che puoi trasformarlo in un due-liner:

context.Widgets.Where(w => w.WidgetId == widgetId)
               .ToList().ForEach(context.Widgets.DeleteObject);
context.SaveChanges();

76
Stai facendo una ToList () che sconfigge lo scopo. In che modo differisce dalla soluzione originale?
lahsrah,

3
Ho dei problemi dato che ho solo il metodo Remove nell'oggetto contestuale.
Pnct

2
Questa non è sicuramente una soluzione adatta quando ci si aspetta un milione di righe (o anche poche centinaia). Tuttavia, se sappiamo per certo che ci saranno solo poche righe, questa soluzione è pulita e funziona perfettamente. Sì, comporterebbe alcuni round trip verso il DB, ma a mio avviso l'astrazione persa coinvolta nella chiamata di SQL supera direttamente i vantaggi.
Yogster,

Entity Framework, come suggerisce il nome, funziona meglio con i dati a livello di entità. Le operazioni di dati in blocco sono gestite al meglio da buoni vecchi processori memorizzati. Per quanto riguarda le prestazioni, sono di gran lunga le migliori opzioni e batteranno qualsiasi logica EF che richieda un loop.
Paceman,

72
using (var context = new DatabaseEntities())
{
    context.ExecuteStoreCommand("DELETE FROM YOURTABLE WHERE CustomerID = {0}", customerId);
}

Ma come puoi farlo con un elenco di ID? Questa soluzione non gestisce molto bene gli "elenchi".
JesseNewman19

11
@ JesseNewman19 Se hai già un elenco di ID, usa a WHERE IN ({0}), quindi dovrebbe essere il secondo argomento String.Join(",", idList).
Langdon,

@Langdon che non funzionerà, perché invierà il comando a sql in questo modo: WHERE IN ("1, 2, 3"). Il database quindi genera un errore perché gli è stata passata una stringa anziché un elenco di numeri interi.
JesseNewman19

Vorrei generare una dichiarazione del genere con LINQ. La cosa più vicina che ho trovato era una lib. EntityFramework.Extended
Jaider,

Se si sta utilizzando String.Join, potrebbe essere necessario utilizzare string.Formate passare la stringa SQL già formata al comando. Finché la tua lista ha solo numeri interi, non c'è rischio di attacchi di iniezione. Controlla questa domanda: come posso passare un array a un comando store di esecuzione?
Andrew,

50

So che è abbastanza tardi ma nel caso in cui qualcuno abbia bisogno di una soluzione semplice, la cosa bella è che puoi anche aggiungere la clausola where con essa:

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: appena testato con MSSQL2008.

Aggiornare:

La soluzione sopra non funzionerà quando EF genera l'istruzione sql con parametri , quindi ecco l'aggiornamento per 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);
}

Richiede un po 'di riflessione ma funziona bene.


Che cos'è DbContext? Presumo il tuo contesto di framework di entità generato automaticamente? Non ho un metodo chiamato Set <T>.
Rabbino

@Stealth: Sì, è il tuo contesto di dati EF, io uso prima il codice ma il contesto generato automaticamente dovrebbe essere lo stesso. Ci scusiamo per la dichiarazione errata che dovrebbe essere impostata <T> () (la mia azienda limita l'accesso a Internet Non ho potuto incollare il codice, ho dovuto digitare a mano quindi ...), codici aggiornati :)
Thanh Nguyen

3
Questa è l'unica risposta che risponde effettivamente alla domanda! Ogni altra risposta elimina ogni singolo elemento uno alla volta, incredibile.
Rocklan,

Questa sembra la risposta più corretta. Consente l'eliminazione in modo molto generico e scarica correttamente il lavoro nel database e non nel C #.
JesseNewman19

1
Per tutti i programmatori meno tecnici là fuori, volevo approfondire un po 'di più su come implementare questa soluzione eccellente e generica, perché mi avrebbe risparmiato qualche minuto di tempo! Continua nel prossimo commento ...
jdnew18

30

Per chiunque utilizzi EF5, è possibile utilizzare la seguente libreria di estensioni: https://github.com/loresoft/EntityFramework.Extended

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

3
Ha problemi di prestazioni su tavoli di grandi dimensioni, non utilizzabili nella mia situazione.
Tomas,

@Tomas che tipo di prestazioni emesse hai notato? Quanto era grave il problema e su quanto era grande il tavolo? Qualcun altro può confermarlo?
Anestis Kivranoglou,

È davvero veloce rispetto alle alternative là fuori
Jaider

Non riesco a vedere la Delete()funzione nelle mie entità in EF6.
dotNET

context.Widgets.Where(w => w.WidgetId == widgetId).Delete();è il modo più recente con EntityFramework.Extended
Peter Kerr,

11

Sembra ancora folle dover ritirare qualsiasi cosa dal server solo per eliminarlo, ma almeno tornare indietro solo gli ID è molto più snello che trascinare giù tutte le entità:

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

Fare attenzione: ciò potrebbe non riuscire la convalida dell'entità Entity Framework perché gli Widgetoggetti stub hanno solo una Idproprietà inizializzata . Il modo per aggirare questo è usare context.Configuration.ValidateOnSaveEnabled = false(almeno in EF6). Ciò disabilita la convalida di Entity Framework, ma ovviamente esegue comunque la convalida del database.
Sammy S.

@SammyS. Non l'ho sperimentato, quindi non posso parlare dei dettagli, ma sembra strano che EF si preoccupi della convalida quando cancella comunque la riga.
Edward Brey,

Hai assolutamente ragione. L'ho confuso deletecon una soluzione simile per le updateentità ing senza caricarle.
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>();

7
Questo è effettivamente lo stesso di db.People.RemoveRange (db.People.Where (x => x.State == "CA")); db.SaveChanges (); Quindi nessun guadagno in termini di prestazioni.
ReinierDG

4

Per EF 4.1,

var objectContext = (myEntities as IObjectContextAdapter).ObjectContext;
objectContext.ExecuteStoreCommand("delete from [myTable];");

1
Funziona, ma il punto centrale dell'utilizzo di Entity Framework sta avendo un modo orientato agli oggetti per interagire con il database. Questo è solo direttamente in esecuzione la query SQL.
Arturo Torres Sánchez,

4

Puoi utilizzare le librerie di estensioni per farlo come EntityFramework.Extended o Z.EntityFramework.Plus.EF6, sono disponibili per EF 5, 6 o Core. Queste librerie hanno grandi prestazioni quando devi cancellare o aggiornare e usano LINQ. Esempio per l'eliminazione ( fonte più ):

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

o ( fonte estesa )

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

Questi usano istruzioni SQL native, quindi le prestazioni sono eccezionali.


Paga 600 $ + per il generatore di operazioni sql all'ingrosso. Sul serio?
nicolay.anykienko,

@ nicolay.anykienko Quando l'ho usato, questa libreria era gratuita, ci sono altre operazioni in cui devi pagare, giusto non so se devi pagare
UUHHIVS

3

Il modo più rapido per eliminare è utilizzare una procedura memorizzata. Preferisco le procedure memorizzate in un progetto di database rispetto a SQL dinamico perché le rinominazioni verranno gestite correttamente e avranno errori di compilatore. SQL dinamico potrebbe fare riferimento a tabelle che sono state eliminate / rinominate causando errori di runtime.

In questo esempio, ho due tabelle List e ListItems. Ho bisogno di un modo rapido per eliminare tutti gli ListItems di un determinato elenco.

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

Ora la parte interessante dell'eliminazione degli elementi e l'aggiornamento del framework Entity tramite un'estensione.

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

Il codice principale ora può usarlo è come

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

Grazie per un bell'esempio di utilizzo di una Stored Procedure e quindi dell'implementazione come estensione, con il codice Usage.
Glenn Garson,

3

Se si desidera eliminare tutte le righe di una tabella, è possibile eseguire il comando sql

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

TRUNCATE TABLE (Transact-SQL) Rimuove tutte le righe da una tabella senza registrare le singole eliminazioni di riga. TRUNCATE TABLE è simile all'istruzione DELETE senza clausola WHERE; tuttavia, TRUNCATE TABLE è più veloce e utilizza meno risorse del registro di sistema e delle transazioni.


3
Dovresti anche ricordare che non puoi eseguire truncate tablesu tabelle a cui fa riferimento un vincolo FOREIGN KEY. (È possibile troncare una tabella che ha una chiave esterna che fa riferimento a se stessa.). Documentazione MSDN
banda larga

2

UUHHIVS's è un modo molto elegante e veloce per l'eliminazione batch, ma deve essere usato con cura:

  • generazione automatica della transazione: le sue query saranno racchiuse in una transazione
  • indipendenza dal contesto del database: la sua esecuzione non ha nulla a che fare con context.SaveChanges()

Questi problemi possono essere elusi assumendo il controllo della transazione. Il codice seguente mostra come eliminare in batch e inserire in blocco in modo transazionale:

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

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

Riepilogo :

Rimuove la raccolta di entità specificata dal contesto sottostante l'insieme con ciascuna entità messa nello stato Eliminato in modo tale che verrà eliminata dal database quando viene chiamato SaveChanges.

Osservazioni :

Se System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled è impostato su true (impostazione predefinita), DetectChanges verrà chiamato una volta prima di eliminare qualsiasi entità e non verrà più richiamato. Ciò significa che in alcune situazioni RemoveRange potrebbe funzionare in modo significativamente migliore rispetto alla chiamata Rimuovi più volte. Notare che se esiste un'entità nel contesto nello stato Aggiunto, questo metodo causerà la sua disconnessione dal contesto. Questo perché si presume che un'entità aggiunta non esista nel database in modo tale che tentare di eliminarla non abbia senso.


1

È possibile eseguire query sql direttamente come segue:

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

Per selezionare possiamo usare

using (var context = new MyContext()) 
{ 
    var blogs = context.MyTable.SqlQuery("SELECT * FROM dbo.MyTable").ToList(); 
}

Dato che EF non supporta correttamente la mappatura delle condizioni di cancellazione, questa è probabilmente la soluzione migliore per portare a termine il lavoro.
Tony O'Hagan,

1

Puoi anche usare il metodo DeleteAllOnSubmit () passandogli i risultati in un elenco generico anziché in var. In questo modo il tuo foreach si riduce a una riga di codice:

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

context.Widgets.DeleteAllOnSubmit(widgetList);

context.SubmitChanges();

Probabilmente usa comunque un loop internamente.


3
Sembra che tu abbia frainteso ciò che varè.
freedomn-m

1

La risposta di Thanh ha funzionato meglio per me. Ho eliminato tutti i miei record in un unico viaggio sul server. Ho faticato a chiamare il metodo di estensione, quindi ho pensato di condividere il mio (EF 6):

Ho aggiunto il metodo di estensione a una classe di supporto nel mio progetto MVC e ho cambiato il nome in "RemoveWhere". Inietto un dbContext nei miei controller, ma potresti anche fare 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);

Ciò ha generato una singola istruzione di eliminazione per il gruppo.


0

EF 6. =>

var assignmentAddedContent = dbHazirBot.tbl_AssignmentAddedContent.Where(a =>
a.HazirBot_CategoryAssignmentID == categoryAssignment.HazirBot_CategoryAssignmentID);
dbHazirBot.tbl_AssignmentAddedContent.RemoveRange(assignmentAddedContent);
dbHazirBot.SaveChanges();

0

Meglio: in EF6 => .RemoveRange()

Esempio:

db.Table.RemoveRange(db.Table.Where(x => Field == "Something"));

14
In che modo differisce dalla risposta di Kyle?

-1

Vedi la risposta 'bit di codice preferito' che funziona

Ecco come l'ho usato:

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

1
In che modo la tua risposta differisce dalla risposta user1308743 ?
Sergey Berezovskiy,

Stavo semplicemente condividendo un esempio funzionante. Qualunque cosa io possa fare per ricambiare l'aiuto che ottengo qui.
Brian Quinn,

-3

In EF 6.2 funziona perfettamente, inviando l'eliminazione direttamente al database senza prima caricare le entità:

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

Con un predicato fisso è abbastanza semplice:

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

E se hai bisogno di un predicato dinamico dai un'occhiata a LINQKit (pacchetto Nuget disponibile), qualcosa del genere funziona bene nel mio 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();

1
Con EF 6.2 grezzo ciò non è possibile. Forse stai usando Z.EntityFramework.Pluso qualcosa di simile? ( entityframework.net/batch-delete )
Sammy S.

Il primo è EF 6.2 grezzo e le opere trovano. Il secondo è, come ho già detto, usando LINQKit.
Vladimir

1
Hmm, non riesco a trovare questo metodo. Potresti verificare su quale classe e in quale spazio dei nomi risiede questo metodo?
Sammy S.

Terzo che (il Delete()metodo è intrinsecamente inesistente).
Somma nessuno
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.