Comment supprimer plusieurs lignes dans Entity Framework (sans foreach)

305

Je supprime plusieurs éléments d'une table à l'aide d'Entity Framework. Il n'y a pas d'objet clé étrangère / parent, je ne peux donc pas gérer cela avec OnDeleteCascade.

En ce moment je fais ça:

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

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

Cela fonctionne mais le foreach me dérange. J'utilise EF4 mais je ne veux pas exécuter SQL. Je veux juste m'assurer de ne rien manquer - c'est aussi bon que possible, non? Je peux l'abstraire avec une méthode d'extension ou un assistant, mais quelque part, nous allons toujours faire un foreach, non?

Jon Galloway
la source
1
Vous voudrez peut-être revoir la réponse acceptée.
Eric J.
1
Si vous voulez rester performant, vous voudrez peut-être vérifier ma réponse ici stackoverflow.com/a/35033286/274589
Adi

Réponses:

49

Si vous ne voulez pas exécuter SQL directement en appelant DeleteObject dans une boucle, c'est le mieux que vous puissiez faire aujourd'hui.

Cependant, vous pouvez exécuter SQL et toujours le rendre complètement général via une méthode d'extension, en utilisant l'approche que je décris ici .

Bien que cette réponse soit pour 3.5. Pour 4.0, j'utiliserais probablement la nouvelle API ExecuteStoreCommand sous le capot, au lieu de passer à StoreConnection.

Alex James
la source
ExecuteStoreCommand n'est pas une bonne façon. DeleteAllSubmit fonctionne dans linq to sql mais pas dans le framework d'entité. Je veux la même option dans le cadre d'entité.
Hiral
653

EntityFramework 6 a rendu cela un peu plus facile avec .RemoveRange().

Exemple:

db.People.RemoveRange(db.People.Where(x => x.State == "CA"));
db.SaveChanges();
Kyle
la source
31
C'est exactement ce dont nous avons besoin ... Sauf lorsque je l'utilise sur une plage suffisamment large, j'obtiens une exception de mémoire insuffisante! Je pensais que le but de RemoveRange était de transmettre le traitement à la base de données, mais apparemment non.
Samer Adra
c'est WAAAYYYY plus rapide que de définir l'état Supprimé pour chaque entité!
Jerther
54
Bien sûr, cette réponse est plus facile mais en termes de performances, elle pourrait ne pas être excellente. Pourquoi? ce que cette exatly fait est la même que la supprimer dans la boucle foreach, elle récupère d'abord toutes les lignes et ensuite la suppression est une par une, seul le gain est pour la sauvegarde "DetectChanges sera appelé une fois avant de supprimer toutes les entités et ne sera plus appelé" rest est identique, essayez d'utiliser l'outil pour voir sql généré.
Anshul Nigam
6
Pour une plage suffisamment large, essayez quelque chose comme .Take (10000) et boucle jusqu'à RemoveRange (...). Count () == 0.
Eric J.
20
Le problème est que le paramètre d'entrée RemoveRange est un IEnumerable donc pour effectuer la suppression, il énumère toutes les entités et exécute 1 requête DELETE par entité.
bubi
74

c'est aussi bon que possible, non? Je peux l'abstraire avec une méthode d'extension ou un assistant, mais quelque part, nous allons toujours faire un foreach, non?

Eh bien, oui, sauf que vous pouvez en faire une doublure:

context.Widgets.Where(w => w.WidgetId == widgetId)
               .ToList().ForEach(context.Widgets.DeleteObject);
context.SaveChanges();
Klaus Byskov Pedersen
la source
76
Vous faites une ToList () qui va à l'encontre du but. En quoi est-ce différent de la solution d'origine?
lahsrah
3
J'ai des problèmes car je n'ai que la méthode Remove dans un objet contextuel.
Pnct
2
Ce n'est certainement pas une solution appropriée lorsqu'un million de lignes (voire quelques centaines) sont attendues. Cependant, si nous savons avec certitude qu'il n'y aura que quelques lignes, cette solution est soignée et fonctionne parfaitement bien. Oui, cela impliquerait quelques allers-retours vers la base de données, mais à mon avis, l'abstraction perdue impliquée dans l'appel de SQL l'emporte directement sur les avantages.
Yogster
Entity Framework, comme son nom l'indique, fonctionne mieux avec les données au niveau de l'entité. Les opérations de données en masse sont mieux gérées par de bons vieux processus stockés. En termes de performances, ils sont de loin les meilleures options et battront toute logique EF nécessitant une boucle.
Paceman
72
using (var context = new DatabaseEntities())
{
    context.ExecuteStoreCommand("DELETE FROM YOURTABLE WHERE CustomerID = {0}", customerId);
}
Vlad Bezden
la source
Mais comment pouvez-vous faire cela avec une liste d'identifiants? Cette solution ne gère pas très bien les "listes".
JesseNewman19
11
@ JesseNewman19 Si vous avez déjà une liste d'ID, utilisez a WHERE IN ({0}), et le deuxième argument devrait être String.Join(",", idList).
Langdon
@Langdon qui ne fonctionnera pas, car il enverra la commande à sql comme ceci: WHERE IN ("1, 2, 3"). La base de données renvoie ensuite une erreur car vous lui avez transmis une chaîne au lieu d'une liste d'entiers.
JesseNewman19
Je souhaite générer une déclaration comme celle-ci avec LINQ. La chose la plus proche que j'ai trouvée était une lib. EntityFramework.Extended
Jaider
Si vous utilisez String.Join, vous devrez peut-être utiliser string.Formatet transmettre la chaîne SQL déjà formée à la commande. Tant que votre liste n'a que des entiers, il n'y a pas de risque d'attaque par injection. Vérifiez cette question: comment puis-je passer un tableau à une commande d'exécution de magasin?
Andrew
50

Je sais qu'il est assez tard mais si quelqu'un a besoin d'une solution simple, le truc sympa est que vous pouvez également ajouter la clause where avec:

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

Remarque: vient d'être testé avec MSSQL2008.

Mettre à jour:

La solution ci-dessus ne fonctionnera pas lorsque EF génère une instruction sql avec des paramètres , alors voici la mise à jour pour 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);
}

Il nécessite un peu de réflexion mais fonctionne bien.

Thanh Nguyen
la source
Qu'est-ce que DbContext? Je suppose que votre contexte de structure d'entité généré automatiquement? Je n'ai pas de méthode appelée Set <T>.
Stealth Rabbi
@Stealth: Oui, c'est votre contexte de données EF, j'utilise d'abord le code, mais le contexte généré automatiquement devrait être le même. Désolé pour la déclaration mal tapée, elle devrait être définie <T> () (mon entreprise restreint l'accès à Internet, je n'ai pas pu coller le code, j'ai dû taper à la main donc ...), les codes ont été mis à jour :)
Thanh Nguyen
3
C'est la seule réponse qui répond réellement à la question! Chaque autre réponse supprime chaque élément individuellement un à la fois, incroyable.
Rocklan
Cela semble être la réponse la plus correcte. Il permet la suppression de manière très générique et décharge correctement le travail vers la base de données et non vers le C #.
JesseNewman19
1
Pour tous les programmeurs moins techniques, je voulais en savoir un peu plus sur la façon de mettre en œuvre cette excellente solution générique, car cela m'aurait fait gagner quelques minutes! Suite dans le commentaire suivant ...
jdnew18
30

Pour toute personne utilisant EF5, la bibliothèque d'extensions suivante peut être utilisée: https://github.com/loresoft/EntityFramework.Extended

context.Widgets.Delete(w => w.WidgetId == widgetId);
Marcelo Mason
la source
3
A des problèmes de performances sur de grandes tables, non utilisables dans ma situation.
Tomas
@Tomas quel type de performance émis avez-vous remarqué? Quelle était la gravité du problème et quelle était la taille de la table? Quelqu'un d'autre peut le confirmer?
Anestis Kivranoglou
C'est vraiment rapide comparé aux alternatives
Jaider
Je ne vois pas de Delete()fonction dans mes entités dans EF6.
dotNET
context.Widgets.Where(w => w.WidgetId == widgetId).Delete();est la manière la plus récente avec EntityFramework.Extended
Peter Kerr
11

Il semble toujours fou de devoir retirer quoi que ce soit du serveur pour le supprimer, mais au moins récupérer uniquement les identifiants est beaucoup plus simple que de retirer les entités complètes:

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
la source
Soyez prudent - cela peut échouer lors de la validation d'entité d'Entity Framework car vos Widgetobjets de stub n'ont qu'une Idpropriété initialisée . La solution consiste à utiliser context.Configuration.ValidateOnSaveEnabled = false(au moins dans EF6). Cela désactive la propre validation d'Entity Framework, mais effectue bien sûr la propre validation de la base de données.
Sammy S.Le
@SammyS. Je n'ai pas vécu cela, donc je ne peux pas parler des détails, mais il semble étrange qu'EF se soucie de la validation quand il supprime la ligne de toute façon.
Edward Brey
Vous avez absolument raison. J'ai confondu le deleteavec une solution de contournement similaire pour updateles entités sans les charger.
Sammy S.Le
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();
} 

Usage:

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

Or:

// delete all from entity
DeleteWhere<MyEntity>();
mnsr
la source
7
C'est en fait la même chose que db.People.RemoveRange (db.People.Where (x => x.State == "CA")); db.SaveChanges (); Donc pas de gain de performances.
ReinierDG
4

Pour EF 4.1,

var objectContext = (myEntities as IObjectContextAdapter).ObjectContext;
objectContext.ExecuteStoreCommand("delete from [myTable];");
Amit Pawar
la source
1
Cela fonctionne, mais l'intérêt d'utiliser Entity Framework est d'avoir une manière orientée objet d'interagir avec la base de données. Il s'agit simplement d'exécuter directement la requête SQL.
Arturo Torres Sánchez
4

Vous pouvez utiliser des bibliothèques d'extensions pour cela, comme EntityFramework.Extended ou Z.EntityFramework.Plus.EF6, elles sont disponibles pour EF 5, 6 ou Core. Ces bibliothèques ont d'excellentes performances lorsque vous devez supprimer ou mettre à jour et qu'elles utilisent LINQ. Exemple de suppression ( source plus ):

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

ou ( source étendue )

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

Ceux-ci utilisent des instructions SQL natives, donc les performances sont excellentes.

UUHHIVS
la source
Payez 600 $ + pour le générateur d'opération sql en vrac. Sérieusement?
nicolay.anykienko
@ nicolay.anykienko Quand je l'ai utilisée, cette bibliothèque était gratuite, il y a d'autres opérations où vous devez payer, non, je ne sais pas si vous devez payer
UUHHIVS
3

La façon la plus rapide de supprimer consiste à utiliser une procédure stockée. Je préfère les procédures stockées dans un projet de base de données au SQL dynamique car les renommages seront traités correctement et comportent des erreurs de compilation. Dynamic SQL peut faire référence à des tables qui ont été supprimées / renommées, provoquant des erreurs d'exécution.

Dans cet exemple, j'ai deux tables List et ListItems. J'ai besoin d'un moyen rapide pour supprimer tous les ListItems d'une liste donnée.

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

Maintenant, la partie intéressante de la suppression des éléments et de la mise à jour du framework Entity à l'aide d'une extension.

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

Le code principal peut maintenant l'utiliser comme

[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
la source
Merci pour un bel exemple d'utilisation d'une procédure stockée, puis de l'implémenter en tant qu'extension, avec le code d'utilisation.
glenn garson
3

Si vous souhaitez supprimer toutes les lignes d'une table, vous pouvez exécuter la commande sql

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

TRUNCATE TABLE (Transact-SQL) Supprime toutes les lignes d'une table sans enregistrer les suppressions de lignes individuelles. TRUNCATE TABLE est similaire à l'instruction DELETE sans clause WHERE; cependant, TRUNCATE TABLE est plus rapide et utilise moins de ressources système et de journal des transactions.

Amir
la source
3
Vous devez également mentionner que vous ne pouvez pas exécuter truncate tablesur des tables référencées par une contrainte FOREIGN KEY. (Vous pouvez tronquer une table qui a une clé étrangère qui se référence elle-même.). Documentation MSDN
haut débit
2

UUHHIVSest un moyen très élégant et rapide pour la suppression par lots, mais il doit être utilisé avec précaution:

  • génération automatique de transaction: ses requêtes seront englobées par une transaction
  • indépendance du contexte de la base de données: son exécution n'a rien à voir avec context.SaveChanges()

Ces problèmes peuvent être contournés en prenant le contrôle de la transaction. Le code suivant illustre comment supprimer par lot et insérer en bloc de manière transactionnelle:

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

Résumé :

Supprime la collection d'entités donnée du contexte sous-jacent à l'ensemble, chaque entité étant mise à l'état Supprimé de sorte qu'elle sera supprimée de la base de données lors de l'appel de SaveChanges.

Remarques :

Notez que si System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled est défini sur true (qui est la valeur par défaut), alors DetectChanges sera appelé une fois avant de supprimer toutes les entités et ne sera plus appelé. Cela signifie que, dans certaines situations, RemoveRange peut être beaucoup plus performant que d'appeler Remove plusieurs fois. Notez que si une entité existe dans le contexte à l'état Ajouté, cette méthode entraînera son détachement du contexte. Cela est dû au fait qu'une entité ajoutée est supposée ne pas exister dans la base de données, de sorte que tenter de la supprimer n'a aucun sens.

Nguyen Van Thanh
la source
1

Vous pouvez exécuter des requêtes SQL directement comme suit:

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

Pour certains, nous pouvons utiliser

using (var context = new MyContext()) 
{ 
    var blogs = context.MyTable.SqlQuery("SELECT * FROM dbo.MyTable").ToList(); 
}
Abhishek Sharma
la source
Étant donné que EF ne prend pas correctement en charge le mappage des conditions de suppression, c'est probablement votre meilleur pari pour faire le travail.
Tony O'Hagan
1

Vous pouvez également utiliser la méthode DeleteAllOnSubmit () en lui passant vos résultats dans une liste générique plutôt que dans var. De cette façon, votre foreach se réduit à une seule ligne de code:

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

context.Widgets.DeleteAllOnSubmit(widgetList);

context.SubmitChanges();

Cependant, il utilise probablement une boucle en interne.

Hugo Nava Kopp
la source
3
On dirait que vous ne comprenez pas ce qu'est un var.
freedomn-m
1

La réponse de Thanh a fonctionné le mieux pour moi. Supprimé tous mes enregistrements en un seul voyage sur le serveur. J'ai eu du mal à appeler la méthode d'extension, alors j'ai pensé partager la mienne (EF 6):

J'ai ajouté la méthode d'extension à une classe d'assistance dans mon projet MVC et j'ai changé le nom en "RemoveWhere". J'injecte un dbContext dans mes contrôleurs, mais vous pouvez également faire 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);

Cela a généré une seule instruction de suppression pour le groupe.

Steve Greene
la source
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
la source
0

Meilleur : in EF6 => .RemoveRange()

Exemple:

db.Table.RemoveRange(db.Table.Where(x => Field == "Something"));
maXXis
la source
14
En quoi est-ce différent de la réponse de Kyle?
-1

Voir la réponse «morceau de code préféré» qui fonctionne

Voici comment je l'ai utilisé:

     // 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
la source
1
En quoi votre réponse diffère de la réponse de l'utilisateur 1308743 ?
Sergey Berezovskiy
Je partageais simplement un exemple de travail. Tout ce que je peux faire pour donner en retour l'aide que je reçois ici.
Brian Quinn
-3

Dans EF 6.2, cela fonctionne parfaitement, en envoyant la suppression directement à la base de données sans charger d'abord les entités:

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

Avec un prédicat fixe, c'est assez simple:

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

Et si vous avez besoin d'un prédicat dynamique, jetez un œil à LINQKit (package Nuget disponible), quelque chose comme ça fonctionne bien dans mon cas:

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
la source
1
Avec EF 6.2 brut, ce n'est pas possible. Peut-être que vous utilisez Z.EntityFramework.Plusou quelque chose de similaire? ( entityframework.net/batch-delete )
Sammy S.
Le premier est EF 6.2 brut et fonctionne. La deuxième consiste, comme je l'ai mentionné, à utiliser LINQKit.
Vladimir
1
Hmm, je ne trouve pas cette méthode. Pourriez-vous vérifier sur quelle classe et dans quel espace de noms cette méthode réside?
Sammy
Je tiers cela (la Delete()méthode est intrinsèquement inexistante).
Sum None