Как удалить несколько строк в Entity Framework (без foreach)

305

Я удаляю несколько элементов из таблицы, используя Entity Framework. Нет внешнего ключа / родительского объекта, поэтому я не могу справиться с этим с помощью OnDeleteCascade.

Прямо сейчас я делаю это:

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

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

Это работает, но foreach вызывает меня. Я использую EF4, но я не хочу выполнять SQL. Я просто хочу убедиться, что я ничего не пропустил - это так хорошо, как получается, верно? Я могу абстрагировать его с помощью метода расширения или помощника, но где-то мы все еще будем делать foreach, верно?

Джон Галлоуэй
источник
1
Вы можете вернуться к принятому ответу.
Эрик Дж.
1
Если вы хотите оставаться в курсе, возможно, вы захотите проверить мой ответ здесь stackoverflow.com/a/35033286/274589
Ади

Ответы:

49

Если вы не хотите выполнять SQL напрямую, вызов DeleteObject в цикле - это лучшее, что вы можете сделать сегодня.

Однако вы можете выполнить SQL и сделать его полностью универсальным с помощью метода расширения, используя подход, который я описал здесь .

Хотя этот ответ был для 3,5. Для 4.0 я бы, вероятно, использовал новый API-интерфейс ExecuteStoreCommand, вместо того, чтобы переходить к StoreConnection.

Алекс Джеймс
источник
ExecuteStoreCommand - неправильный способ. DeleAllSubmit работает в linq to sql, но не в рамках сущностей. Я хочу такой же вариант в рамках сущности.
Hiral
653

EntityFramework 6 сделал это немного проще с .RemoveRange().

Пример:

db.People.RemoveRange(db.People.Where(x => x.State == "CA"));
db.SaveChanges();
рукав моря
источник
31
Это именно то, что нам нужно ... За исключением случаев, когда я использую его в достаточно большом диапазоне, я получаю исключение нехватки памяти! Я думал, что весь смысл RemoveRange состоял в том, чтобы передать обработку в базу данных, но, очевидно, нет.
Самер Адра
это WAAAYYYY быстрее, чем установка состояния Deleted для каждой сущности!
Джертер
54
Конечно, этот ответ проще, но с точки зрения производительности он может быть не очень хорошим. Зачем? то, что он делает, точно так же, как удаление его в цикле foreach, сначала он выбирает все строки, а затем удаляет по одной, только выигрыш для сохранения: «DetectChanges будет вызываться один раз перед удалением любых объектов и больше не будет вызываться» rest то же самое, попробуйте использовать инструмент, чтобы увидеть сгенерированный sql.
Аншул Нигам
6
Для достаточно большого диапазона попробуйте что-то вроде .Take (10000) и циклическое выполнение до RemoveRange (...). Count () == 0.
Eric J.
20
Проблема заключается в том, что входной параметр RemoveRange является IEnumerable, поэтому для выполнения удаления он перечисляет все сущности и запускает 1 запрос DELETE для каждой сущности.
Буби
74

это так хорошо, как получается, верно? Я могу абстрагировать его с помощью метода расширения или помощника, но где-то мы все еще будем делать foreach, верно?

Ну, да, кроме того, что вы можете сделать это в два ряда:

context.Widgets.Where(w => w.WidgetId == widgetId)
               .ToList().ForEach(context.Widgets.DeleteObject);
context.SaveChanges();
Клаус Бысков Педерсен
источник
76
Вы делаете ToList (), который побеждает цель. Чем это отличается от оригинального решения?
Лахра
3
У меня проблемы, так как у меня есть только метод Remove в объекте контекста.
Пн
2
Это определенно не подходящее решение, когда ожидается миллион строк (или даже несколько сотен). Однако, если мы точно знаем, что будет только несколько строк, это решение будет опрятным и работает отлично. Да, это потребовало бы нескольких обращений к БД, но, на мой взгляд, потерянная абстракция, связанная с вызовом SQL, напрямую перевешивает преимущества.
Йогстер
Entity Framework, как следует из названия, лучше всего работает с данными на уровне объекта. Операции с большими объемами данных лучше всего обрабатываются старыми добрыми хранимыми процессами. По производительности они являются лучшими вариантами и будут превосходить любую логику EF, требующую цикла.
Paceman
72
using (var context = new DatabaseEntities())
{
    context.ExecuteStoreCommand("DELETE FROM YOURTABLE WHERE CustomerID = {0}", customerId);
}
Влад Безден
источник
Но как вы можете сделать это с помощью списка идентификаторов? Это решение не очень хорошо обрабатывает «списки».
JesseNewman19
11
@ JesseNewman19 Если у вас уже есть список идентификаторов, используйте a WHERE IN ({0}), и тогда второй аргумент должен быть String.Join(",", idList).
Лэнгдон
@Langdon, который не будет работать, потому что он отправит команду в sql следующим образом: WHERE IN ("1, 2, 3"). Затем база данных выдает ошибку, потому что вы передали ей строку вместо списка целых чисел.
JesseNewman19
Я хочу создать такое заявление с помощью LINQ. Самым близким, что я нашел, была библиотека. EntityFramework.Extended
Jaider
Если вы используете String.Join, вам может потребоваться использовать string.Formatи передать уже сформированную строку SQL в команду. Пока в вашем списке только целые числа, нет риска инъекционной атаки. Проверьте этот вопрос: как я могу передать массив команде execute store?
Андрей
50

Я знаю, что уже поздно, но если кому-то нужно простое решение, круто то, что вы также можете добавить к нему предложение where:

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

Примечание: только что протестировано с MSSQL2008.

Обновить:

Приведенное выше решение не будет работать, когда EF генерирует SQL-оператор с параметрами , поэтому вот обновление для 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);
}

Это требует немного размышлений, но работает хорошо.

Тхань Нгуен
источник
Что такое DbContext? Я полагаю, ваш автоматически сгенерированный контекст сущности? У меня нет метода с именем Set <T>.
Рабби-
@Stealth: Да, это ваш контекст данных EF, я использую сначала код, но автоматически сгенерированный контекст должен быть таким же. Извините за неверно введенное утверждение, это должно быть Set <T> () (моя компания ограничивает доступ в Интернет, я не мог вставить код, пришлось печатать вручную, так что ...), коды обновлены :)
Thanh Nguyen
3
Это единственный ответ, который на самом деле отвечает на вопрос! Любой другой ответ удаляет каждый отдельный элемент по одному, невероятно.
Rocklan
Это похоже на самый правильный ответ. Это позволяет удалять очень общим способом, и это правильно выгружает работу в базу данных, а не в C #.
JesseNewman19
1
Для всех менее технических программистов я хотел бы немного подробнее рассказать о том, как реализовать это превосходное и универсальное решение, потому что оно сэкономило бы мне несколько минут времени! Продолжение в следующем комментарии ...
jdnew18
30

Для тех, кто использует EF5, может быть использована следующая библиотека расширений: https://github.com/loresoft/EntityFramework.Extended

context.Widgets.Delete(w => w.WidgetId == widgetId);
Марсело Мейсон
источник
3
Имеет проблемы с производительностью на больших таблицах, не может быть использовано в моей ситуации.
Томас
@ Томас, какой вид исполнения вы заметили? Насколько серьезной была проблема и насколько большой была таблица? Кто-нибудь еще может это подтвердить?
Анестис Кивраноглу
Это действительно быстро по сравнению с альтернативами там
Jaider
Я не вижу Delete()функции в моих сущностях в EF6.
dotNET
context.Widgets.Where(w => w.WidgetId == widgetId).Delete();это новый способ с EntityFramework.Extended
Питер Керр
11

Тем не менее, кажется сумасшедшим, что приходится что-то вытаскивать с сервера только для того, чтобы удалить его, но, по крайней мере, вернуть только идентификаторы намного проще, чем сносить полные сущности:

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 });
Эдвард Брей
источник
Будьте осторожны - это может не Widgetпройти проверку сущности Entity Framework, поскольку ваши объекты- заглушки имеют только инициализированное Idсвойство. Способ обойти это использовать context.Configuration.ValidateOnSaveEnabled = false(по крайней мере, в EF6). Это отключает собственную проверку Entity Framework, но, конечно, выполняет собственную проверку базы данных.
Сэмми С.
@SammyS. Я не сталкивался с этим, поэтому не могу говорить о деталях, но кажется странным, что EF будет беспокоиться о проверке, когда все равно удаляет строку.
Эдвард Брей,
Ты абсолютно прав. Я перепутал deleteаналогичный обходной путь для создания updateсущностей без их загрузки.
Сэмми С.
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();
} 

Использование:

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

Or:

// delete all from entity
DeleteWhere<MyEntity>();
мрин
источник
7
Фактически это то же самое, что и db.People.RemoveRange (db.People.Where (x => x.State == "CA")); db.SaveChanges (); Так что нет прироста производительности.
ReinierDG
4

Для EF 4.1

var objectContext = (myEntities as IObjectContextAdapter).ObjectContext;
objectContext.ExecuteStoreCommand("delete from [myTable];");
Амит Павар
источник
1
Это работает, но весь смысл использования Entity Framework заключается в объектно-ориентированном способе взаимодействия с базой данных. Это просто прямой запуск SQL-запроса.
Артуро Торрес Санчес
4

Для этого вы можете использовать библиотеки расширений, такие как EntityFramework.Extended или Z.EntityFramework.Plus.EF6, которые доступны для EF 5, 6 или Core. Эти библиотеки имеют отличную производительность, когда вам нужно удалить или обновить, и они используют LINQ. Пример для удаления ( источник плюс ):

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

или ( источник расширен )

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

В них используются нативные операторы SQL, поэтому производительность великолепна.

UUHHIVS
источник
Заплатите 600 $ + за массовую работу генератора sql. Шутки в сторону?
nicolay.anykienko
@ nicolay.anykienko Когда я использовал ее, эта библиотека была бесплатной, есть другие операции, где вы должны заплатить, не так ли, я не знаю, нужно ли вам платить
UUHHIVS
3

Самый быстрый способ удалить это использовать хранимую процедуру. Я предпочитаю хранимые процедуры в проекте базы данных, а не динамический SQL, потому что переименования будут обрабатываться правильно и иметь ошибки компилятора. Динамический SQL может ссылаться на таблицы, которые были удалены / переименованы, вызывая ошибки во время выполнения.

В этом примере у меня есть две таблицы List и ListItems. Мне нужен быстрый способ удалить все элементы списка данного списка.

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

Теперь интересная часть удаления элементов и обновления Entity Framework с использованием расширения.

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

Основной код теперь можно использовать как

[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);
    }
}
Ксавье Джон
источник
Спасибо за хороший пример использования хранимой процедуры, а затем ее реализации в виде расширения с использованием кода использования.
Гленн Гарсон
3

Если вы хотите удалить все строки таблицы, вы можете выполнить команду sql

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

TRUNCATE TABLE (Transact-SQL) Удаляет все строки из таблицы без регистрации удалений отдельных строк. TRUNCATE TABLE похож на оператор DELETE без предложения WHERE; однако TRUNCATE TABLE работает быстрее и использует меньше ресурсов системы и журнала транзакций.

эмир
источник
3
Также следует отметить, что вы не можете работать truncate tableс таблицами, на которые ссылается ограничение FOREIGN KEY. (Вы можете усечь таблицу с внешним ключом, который ссылается на себя.). Документация MSDN
широкополосный
2

UUHHIVSЭто очень элегантный и быстрый способ удаления пакетов, но его следует использовать с осторожностью:

  • автоматическое создание транзакции: ее запросы будут охватываться транзакцией
  • независимость контекста базы данных: ее выполнение не имеет ничего общего с context.SaveChanges()

Эти проблемы можно обойти, взяв под контроль транзакцию. Следующий код иллюстрирует, как пакетное удаление и массовая вставка транзакционным способом:

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

Резюме :

Удаляет заданную коллекцию сущностей из контекста, лежащего в основе набора, при котором каждая сущность переводится в состояние «Удалено», так что она будет удалена из базы данных при вызове SaveChanges.

Замечания :

Обратите внимание, что если для System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled задано значение true (по умолчанию), то DetectChanges будет вызываться один раз перед удалением любых сущностей и больше не будет вызываться. Это означает, что в некоторых ситуациях RemoveRange может работать значительно лучше, чем вызов Remove несколько раз. Обратите внимание, что если какая-либо сущность существует в контексте в состоянии «Добавлено», этот метод приведет к ее отсоединению от контекста. Это связано с тем, что предполагается, что добавленная сущность не существует в базе данных, поэтому попытки ее удаления не имеют смысла.

Нгуен Ван Тхань
источник
1

Вы можете выполнять SQL-запросы напрямую следующим образом:

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

Для выбора мы можем использовать

using (var context = new MyContext()) 
{ 
    var blogs = context.MyTable.SqlQuery("SELECT * FROM dbo.MyTable").ToList(); 
}
Абхишек Шарма
источник
Учитывая, что EF должным образом не поддерживает отображение условий удаления, это, вероятно, ваш лучший выбор для выполнения работы.
Тони О'Хаган
1

Вы также можете использовать метод DeleteAllOnSubmit () , передав его результаты в общий список, а не в var. Таким образом, ваш foreach сводится к одной строке кода:

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

context.Widgets.DeleteAllOnSubmit(widgetList);

context.SubmitChanges();

Вероятно, он все еще использует цикл внутри.

Хьюго Нава Копп
источник
3
Похоже, вы не понимаете, что это varтакое.
Libern-м
1

Ответ Тана работал лучше всего для меня. Удалил все мои записи за одну поездку на сервер. Я боролся с фактическим вызовом метода расширения, поэтому думал, что поделюсь своим (EF 6):

Я добавил метод расширения к вспомогательному классу в своем проекте MVC и изменил имя на «RemoveWhere». Я вставляю dbContext в мои контроллеры, но вы также можете сделать 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);

Это сгенерировало один оператор удаления для группы.

Стив Грин
источник
0

EF 6. =>

var assignmentAddedContent = dbHazirBot.tbl_AssignmentAddedContent.Where(a =>
a.HazirBot_CategoryAssignmentID == categoryAssignment.HazirBot_CategoryAssignmentID);
dbHazirBot.tbl_AssignmentAddedContent.RemoveRange(assignmentAddedContent);
dbHazirBot.SaveChanges();
Эрчин Дедеоглу
источник
0

Лучший : in EF6 => .RemoveRange()

Пример:

db.Table.RemoveRange(db.Table.Where(x => Field == "Something"));
Maxxis
источник
14
Чем это отличается от ответа Кайла?
-1

Смотрите ответ «любимый кусочек кода», который работает

Вот как я это использовал:

     // 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
Чем ваш ответ отличается от ответа пользователя user1308743 ?
Сергей Березовский
Я просто поделился рабочим примером. Все, что я могу сделать, чтобы вернуть за помощь, которую я получил здесь.
Брайан Куинн
-3

В EF 6.2 это работает отлично, отправляя удаление непосредственно в базу данных без предварительной загрузки сущностей:

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

С фиксированным предикатом это довольно просто:

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

И если вам нужен динамический предикат, взгляните на LINQKit (доступен пакет Nuget), в моем случае что-то вроде этого работает нормально:

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
С необработанным EF 6.2 это невозможно. Может быть, вы используете Z.EntityFramework.Plusили что-то подобное? ( entityframework.net/batch-delete )
Сэмми С.
Первый - это сырой EF 6.2 и поиск работы. Второй, как я уже говорил, использует LINQKit.
Владимир
1
Хм, я не могу найти этот метод. Не могли бы вы проверить, в каком классе и в каком пространстве имен находится этот метод?
Сэмми С.
Я третий, что ( Delete()метод по существу не существует).
Сумма отсутствует