Ядро EF обновляет объект и дочерние объекты в цикле — сохраняет изменения, фиксирующие весь список при первом проходе

#c# #entity-framework #entity-framework-core

#c# #entity-framework #entity-framework-core

Вопрос:

Я хотел бы обновить одно поле в родительской таблице и добавить дочерние записи в цикл foreach. Идея состоит в том, чтобы гарантировать, что каждый набор записей обновляется независимо, потому что каждый родительский элемент может содержать более 100 тыс. дочерних элементов, и если один из них завершается неудачей, другие все равно должны быть повторены.

Родительские записи уже существуют в базе данных, и только одно поле должно быть обновлено, а дочерних записей еще не существует.

Проблема в том, что первый проход в цикле for each фактически фиксирует изменения для всех элементов коллекции, а не для одной родительской записи и ее дочерних элементов. Другие проходы вызывают исключение дубликата первичного ключа, поскольку он снова пытается обновить и вставить все элементы. Это похоже на то, как если бы foreach игнорировался, и все передавалось сразу, что является слишком большой нагрузкой и не гарантирует, что каждый родительский элемент, дочерний элемент является изолированной транзакцией.

Что может быть причиной такого поведения?

  private async Task<IEnumerable<ParentTable>> SaveChildItems(IEnumerable<ParentTable> itemsToBeSaved)
        {
            var res = new List<ParentTable>();
            foreach(var itemToSave in itemsToBeSaved)
            {
                var strategy = _dbContext.Database.CreateExecutionStrategy();
                await strategy.ExecuteAsync(async() => 
                {
                    using (var transaction = _dbContext.Database.BeginTransaction())
                    {
                        try
                        {
                            var resFile = await _dbContext.ParentTable.Where(x => x.Name == itemsToBeSaved.Name).FirstOrDefaultAsync();
                                resFile.FieldToUpdate = enum.UpdateValue;
                                resFile.ChildTable = itemsToBeSaved.ChildTable;
                            
                            await _dbContext.SaveChangesAsync();
                            transaction.Commit();

                            res.Add(itemToSave);
                        }
                        catch (System.Exception ex)
                        {
                            transaction.Rollback();
                            Log.Fatal(ex, LoggingTemplates.Exception, "Error on parent table file. "   ex.Message, @"n", ex);
                        }
                    }
                });
            }
          
             return res;
        }
 

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

  private async Task<IEnumerable<ParentTable>> SaveRecords(IEnumerable<ParentTable> itemsToBeSaved)
        {
            var res = new List<ParentTable>();
            foreach(var itemToSave in itemsToBeSaved)
            {
                var strategy = _dbContext.Database.CreateExecutionStrategy();
                await strategy.ExecuteAsync(async() => 
                {
                    using (var transaction = _dbContext.Database.BeginTransaction())
                    {
                        try
                        {
                            var resFile = await _dbContext.ParentTable.AddAsync(itemToSave);
                            await _dbContext.SaveChangesAsync();
                            transaction.Commit();

                            res.Add(itemToSave);
                        }
                        catch (System.Exception ex)
                        {
                            transaction.Rollback();
                            Log.Fatal(ex, LoggingTemplates.Exception, "Error on parent table file. "   ex.Message, @"n", ex);
                        }
                    }
                });
            }
          
             return res;
        }
 

Комментарии:

1. Не уверен, но попробуйте скопировать переменную itemToSave в var itemToSaveLocal = itemToSave и использовать новую переменную внутри лямбда. Это может быть плохое закрытие. Кроме того, если элементов много, попробуйте воссоздать DbContext для каждой итерации, чтобы ускорить следующие вставки.

2. Спасибо @SvyatoslavDanyliv за комментарий. Пробовал, но не получилось. Я отредактировал исходное сообщение с помощью кода, который работает для добавления родительской записи с дочерними объектами без обновления.

3. Происходит что-то странное. Похоже, вы извлекаете записи из _dbContext, манипулируете записями, но затем вызываете SaveChangesAsync для _appDbContext (другой контекст).

4. Спасибо за уловку @NeilW, но это была просто опечатка при обобщении кода. В приложении у меня есть один контекст. Я отредактировал исходное сообщение.

5. Ах, хорошо. Я думаю, то же самое относится и к itemsToBeSaved . Имя и элементы сохраняются. Дочерняя таблица.

Ответ №1:

Я предполагаю, что «родительские» таблицы в itemsToBeSaved, передаваемые в функцию, были извлечены из того же экземпляра _dbContext .

В этом случае все они отслеживаются этим контекстом и как только вы вызываете

 await _dbContext.SaveChangeAsync()
 

в первый раз EF автоматически обновит все, что отслеживается в этом контексте.

Передаваемые «itemsToBeSaved» необходимо отменить отслеживание.

Это то, что происходит:

 // Retrieve 2 tables (tracked by the _dbContext))
var parents = _dbContext.ParentTable.Where(x => x.Name == "foo" || x.Name == "bar");

// Assign to two different vars
var tableFoo = parents.where(x => x.Name == "foo");
var tableBar = parents.where(x => x.Name == "bar");

// Put a child table on first one
tableFoo.ChildTable = new ChildTable() { Name = "Fum" };

// Will save ALL parent tables originally retrieved
// and the child table added so far in one go
await _dbContext.SaveChangesAsync();

// Too LATE.  tableBar has already been updated.
tableBar.ChildTable = new ChildTable() { Name = "Too" };
 

Я думаю, что вы, возможно, неправильно используете EF.

Попробуйте:

 // Get the parent tables out of the database (without tracking).  They're now not 'attached'.
var parents = _dbContext.ParentTable.AsNoTracking().Where(x => x.Name == "foo" || x.Name == "bar").ToList();
 

Затем удалите этот _dbContext и выполните всю работу за пределами функции, в которую вы вносите свои изменения.

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

В следующем цикле повторите то же самое.

Сохранение одного экземпляра _dbContext на уровне модуля класса может показаться логичным, но это немного «ошибка». Лучше всего создавать (и удалять) контексты по мере необходимости.

 foreach ()
{
    // instantiate context with 'using' to enforce disposal
    using (var _dbContext = new DbContext())
    {
        // retrieve records to be updated (NOT with AsNoTracking())
        // make updates to the 'attached' records
        // save changes
        // dispose of context ("using" will automatically call Dispose() at the closing brace
    }
}