FirstOrDefault возвращает ноль, в то время как строка существует - PullRequest
0 голосов
/ 27 сентября 2019

У меня происходит некоторое состояние гонки, когда строка в БД может быть создана двумя потоками одновременно.Чтобы обойти это, я реализовал повторные попытки, например, так:

int retries = 0;
while (true)
{                
    try
    {
        var saved = context.Table.FirstOrDefault(x => x.field1 == val1 && x.field2 == val2);

        if (saved != null)
        {
            //edits saved
        }
        else
        {
            context.Table.Add(new Table
            {
                field1 = val1,
                field2 = val2
            });
        }
        await context.SaveChangesAsync();
        return Json(true);
    }
    catch (Exception e)
    {
        if (retries >= 5)
            throw (e);
        retries++;
    }
}

Почему-то 5 раз подряд происходит сбой с такой же ошибкой:

Microsoft.EntityFrameworkCore.DbUpdateException: Произошла ошибка при обновлении записей.Смотрите внутреннее исключение для деталей.---> System.Data.SqlClient.SqlException: Невозможно вставить повторяющуюся строку ключа в объект 'dbo.Table' с уникальным индексом 'IX_Table_field1_field2'.Значение ключа-дубликата: (val1, val2).

Почему FirstOrDefault возвращает ноль, даже если строка явно существует в базе данных?Я использую Microsoft.AspNetCore.All v.2.1.4

EDIT: для пояснения.Контекст не передается между потоками.Гонка происходит, когда несколько HTTP-запросов поступают одновременно.Контекст вводится в контроллер (где находится этот код).Он был зарегистрирован с помощью вызова AddDbContext с настройками по умолчанию, что делает его ServiceLifetime ограниченным.

РЕШЕНИЕ: комментарий Фениксила дал мне необходимую подсказку.Добавленная, но несохраненная строка остается в контексте и пытается вставить ее.Я сохранил ссылку на новую строку и добавил ее в блок catch:

context.Entry(NewRow).State = EntityState.Detached;

Ответы [ 3 ]

1 голос
/ 27 сентября 2019

Вы поделились DbContext? DbContext не является потокобезопасным .

Попробуйте поместить операцию вставки в using блок DbContext вместо повторной попытки:

using(var context = new DbContext)
{
  // Insert operation here
}

Конфликтэто легко понять, но сначала вам нужно знать, что когда вы await вызываете, поток немедленно возвращается к вызывающей стороне.

Представьте себе этот сценарий, у вас есть два потока, выполняющих ваш код.Вот порядок выполнения:

  1. Поток 1: FirstOrDefault возвращает null.
  2. Поток 2: FirstOrDefault возвращает null.
  3. Поток1: Add работает.SQL генерируется и ставится в очередь на сервере базы данных.
  4. Поток 1: await context.SaveChangesAsync().Вызов завершается немедленно.
  5. База данных: завершен вызов из потока 1.
  6. Поток 2: Add выполняется.SQL генерируется и ставится в очередь на сервере базы данных.
  7. Поток 2: await context.SaveChangesAsync().Вызов завершается немедленно.
  8. База данных: пробный вызов из потока 2, но не может его завершить, поскольку ранее была вставлена ​​строка с таким же значением ключа.
0 голосов
/ 29 сентября 2019

Если вы хотите, чтобы никто не мог добавить запись, когда вы пытаетесь ее найти, вам нужно использовать транзакции :

using (var context = new MyContext())
using (var transaction = context.Database.BeginTransaction(IsolationLevel.Serializable)) {
        var saved = context.Table.FirstOrDefault(x => x.field1 == val1 && x.field2 == val2);
        if (saved != null)
        {
            //edits saved
        }
        else
        {
            context.Table.Add(new Table
            {
                field1 = val1,
                field2 = val2
            });
        }
        await context.SaveChangesAsync();
        transaction.Commit()
        return Json(true);
}

Я использую самый изолированный уровень здесьзаблокировать таблицу и предотвратить гонку условий в чтении.Этот подход влияет на производительность, и если повторная попытка является приемлемой, вы все равно можете следовать этому подходу.Существует отличный фреймворк Polly.NET , который может быть очень полезен для вас:

await Policy.Handle<DbUpdateException>()
            .RetryAsync(5)
            .ExecuteAsync(() => ...);

Я бы не рекомендовал использовать какие-либо внутрипроцессные блокировки для вашего DbContext (или чего-либо еще)) потому что это ограничивает вас для запуска одного процесса с этой логикой, а это не тот случай, когда вам нужна высокая доступность.

0 голосов
/ 27 сентября 2019

Если в базе данных есть запись с val1 в качестве ключа, но val2 отличается, firstOrDefault() не вернет значение, и вы все равно не сможете вставить новую запись.

Это также может быть проблемой кеширования.Вы можете попробовать добавить AsNoTracking() к вашему запросу.

...