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