Как игнорировать исключение DbUpdateConcurrencyException при удалении объекта

У меня есть приложение, которое считывает много данных в память и обрабатывает их партиями.

Я хочу, чтобы структура сущностей игнорировала DbUpdateConcurrencyException при удалении сущности, которая уже была удалена.

Причина в том, что к моменту обработки объекта и его пометки на удаление он уже может быть удален из БД.

Незаметное удаление строки, которая уже была удалена, не является проблемой и не должно вызывать ошибку, мне просто нужен способ сообщить об этом инфраструктуре сущностей :)

Пример

Db.Entry(itemToRemove).State = EntityState.Deleted;
Db.SaveChanges();

Вызывает ошибку, если itemToRemove уже удалено.

Примечание: Db.Configuration.ValidateOnSaveEnabled = false; не исправляет это, как предлагал другой поток.


person herostwist    schedule 10.10.2013    source источник


Ответы (7)


Как насчет?

Db.Entry(itemToRemove).State = EntityState.Deleted;

bool saveFailed;
do
{
    saveFailed = false;
    try
    {
       Db.SaveChanges();
    }
    catch(DbUpdateConcurrencyException ex)
    {
       saveFailed = true;
       var entry = ex.Entries.Single();
       //The MSDN examples use Single so I think there will be only one
       //but if you prefer - do it for all entries
       //foreach(var entry in ex.Entries)
       //{
       if(entry.State == EntityState.Deleted)
          //When EF deletes an item its state is set to Detached
          //http://msdn.microsoft.com/en-us/data/jj592676.aspx
          entry.State = EntityState.Detached;
       else
          entry.OriginalValues.SetValues(entry.GetDatabaseValues());
          //throw; //You may prefer not to resolve when updating
       //}
    }
} while (saveFailed);

Подробнее здесь: Разрешение оптимистичных исключений параллелизма

person Colin    schedule 10.10.2013
comment
выглядит интересно. что произойдет, если во время вызова savechanges будет удалено много элементов. Мне нужно будет отсоединить те, которые были ранее удалены, а затем продолжить сохранение/удаление остальных. - person herostwist; 10.10.2013
comment
SaveChanges выполняется в транзакции, поэтому фактически ничего не удаляется до тех пор, пока не будет исправлен последний элемент, вызывающий проблему параллелизма. - person Colin; 10.10.2013
comment
Ах ха я вижу. В таком случае я не мог бы сделать это... catch(DbUpdateConcurrencyException ex) { ex.Entries.Where(e => e.State == EntityState.Deleted).ToList().ForEach(e => e.State = EntityState .Отдельный); БД.СохранитьИзменения(); } - person herostwist; 10.10.2013
comment
Учитывая, что примеры в MSDN вызывают ex.Entries.Single, я предполагаю, что ex.Entries будет содержать только одну запись, поэтому ваш код будет делать то же самое... но, возможно, вы недовольны этим предположением... Чего я не получаю, так это почему вы думаете, что откат транзакции мешает вам написать это так? - person Colin; 10.10.2013
comment
в соответствии с msdn.microsoft.com/en-us/library/ ex.Entries содержит все объекты, которые не удалось сохранить. Единственная проблема с циклом while заключается в том, что он будет вращаться вечно, если есть объект, который не может быть сохранен по другой причине, кроме entry.State == EntityState.Deleted. - person herostwist; 10.10.2013
comment
спасибо за ваш ответ, хотя, он определенно указал мне правильное направление. - person herostwist; 10.10.2013
comment
Да, поэтому базовый класс DbUpdateException допускает множество записей. Я думаю, что вполне вероятно, что DbUpdateConcurrencyException будет содержать только один - и код отредактирован, чтобы предотвратить бесконечный цикл - person Colin; 10.10.2013

Вы можете обработать DbUpdateConcurrencyException, а затем вызвать Refresh(RefreshMode,IEnumerable) с RefreshMode.StoreWins и вашими удаленными объектами в качестве параметра.

try{
  Db.Entry(itemToRemove).State = EntityState.Deleted;
  Db.SaveChanges();
}
catch(DbUpdateConcurrencyException)
{
  IObjectContextAdapter adapter = Db;

  adapter.ObjectContext.Refresh(RefreshMode.StoreWins, context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Deleted));
  Db.SaveChanges();
}
person Jehof    schedule 10.10.2013
comment
Я просмотрел refresh, но, поскольку он будет выполняться очень большими партиями в многопоточной среде, я не хочу брать на себя дополнительные затраты/затраты на чтение данных во второй раз из базы данных. - person herostwist; 10.10.2013
comment
что мне действительно нужно, так это чтобы при сохранении изменений игнорировались любые ошибки DbUpdateConcurrencyException и завершалась транзакция без необходимости повторного чтения объектов из базы данных. - person herostwist; 10.10.2013

На основе кода из https://msdn.microsoft.com/en-US/data/jj592904, но где я добавил бесконечный счетчик циклов (на всякий случай, вы никогда не знаете, верно?) и перебирает все записи в списке исключений.

var maxTriesCounter = 20;
bool saveFailed;
do
{
    saveFailed = false;
    maxTriesCounter--;
    try
    {
        context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        saveFailed = true;
        foreach (var entry in ex.Entries)
        {
            entry.Reload();
        }
    }
} while (saveFailed && maxTriesCounter > 0);
person cederlof    schedule 04.01.2016

Вот что я использую. Отсоедините все записи о проблемах после сохранения.

Db.Entry(itemToRemove).State = EntityState.Deleted;

while(true)
    try {
        Db.SaveChanges();
        break;
    } catch (DbUpdateConcurrencyException ex) {
        ex.Entries.ToList().ForEach(x=>x.State=EntityState.Detached);
    }

Или вы можете добавить пользовательскую функцию SaveChanges в свой класс DbContext и использовать ее всякий раз, когда вам нужно игнорировать эти ошибки.

    public int SaveChanges_IgnoreConcurrencyExceptions  () {
        while(true)
            try {
                return this.SaveChanges();
            } catch (DbUpdateConcurrencyException ex) {
                ex.Entries.ToList().ForEach(x => x.State=EntityState.Detached);
            }
    }
person Carter Medlin    schedule 06.04.2017
comment
Спасибо. Разве это не то, что я написал в своем комментарии к принятому ответу? - person herostwist; 07.04.2017
comment
Это близко. Структура цикла отличается, и я отделяю каждую неудачную запись вместо того, чтобы ограничивать ее удаленными записями (что могло вызвать бесконечные циклы). Также я предлагаю создать новую функцию SaveChanges. - person Carter Medlin; 07.04.2017

Это мой подход:

    public async Task DeleteItem(int id)
    {
        bool isDone = false;
        while (!isDone)
        {
            var item= await dbContext.Items.AsNoTracking().SingleOrDefaultAsync(x=> x.id== id);
            if (item== null)
                return;

            dbContext.Items.Delete(item);
            try
            {
                await dbContext.CommitAsync();
                return;
            }
            catch (DbUpdateConcurrencyException ex)
            {
            }
        }

    }
person Mohammad Barbast    schedule 30.06.2021

Это другой подход:

        context.Delete(item);
        bool saveFailed;
        do
        {
            saveFailed = false;

            try
            {
                await context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException ex)
            {
                saveFailed = true;
                var entity = ex.Entries.Single();
                await entity.Single().ReloadAsync();

                if (entity.State == EntityState.Unchanged)// entity is already updated
                    context.Delete(item);;
                else if (entity.State == EntityState.Detached) // entity is already deleted
                    saveFailed =false;
            }
        } while (saveFailed);

ReloadAsync() метод от Документы Майкрософт :

Перезагружает объект из базы данных, перезаписывая любые значения свойств значениями из базы данных.

Сущность будет находиться в состоянии Unchanged после вызова этого метода, если сущность не существует в базе данных, и в этом случае сущность будет отключена. Наконец, вызов Reload для добавленного объекта, который не существует в базе данных, не является операцией. Обратите внимание, однако, что добавленный объект может еще не иметь своего постоянного значения ключа.

person Mohammad Barbast    schedule 30.06.2021
comment
var entity = ex.Entries.Single(); вызовет исключение, если было изменено более одной записи. См. docs.microsoft.com/en-us/dotnet/api/ - person herostwist; 30.06.2021
comment
@herostwist, в ситуации, когда мы удаляем объект Principal, это может быть правдой, но если наш объект Dependent, достаточно просто полагаться на метод Single(). - person Mohammad Barbast; 30.06.2021

Я опубликовал этот вопрос давным-давно, но недавно ему было уделено некоторое внимание, поэтому я хотел бы добавить решение, которое я действительно использую.

//retry up to 5 times
for (var retries = 0; retries < 5; retries++)
{
    try
    {
        Db.SaveChanges();
        break;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entity in ex.Entries)
        {
            entity.State = EntityState.Detached;
        }
    }
}

Вещи, которые я рассмотрел - я НЕ хотел использовать ReloadAsync() или ObjectContext.Refresh, поскольку я хотел игнорировать элементы, удаленные в другом процессе, БЕЗ каких-либо дополнительных накладных расходов на базу данных. Я добавил в цикл for как простую защиту от бесконечных циклов - не то, что должно произойти, но я человек с ремнем и фигурными скобками, а не поклонник while(true), если этого можно избежать. Нет необходимости в локальной переменной, такой как isDone или saveFailed - просто сломайте, если мы успешно сохранили. Не нужно приводить ex.Entries к списку, чтобы перечислить его — то, что вы можете написать что-то в одной строке, не делает его лучше.

person herostwist    schedule 02.07.2021