c# - EF Core 在循环中更新实体和子实体 - 在第一次通过时保存提交整个列表的更改
问题描述
我想更新父表上的一个字段并在 foreach 循环中添加子记录。这个想法是确保每组记录都独立更新,因为每个父项可以包含超过 10 万个子项,如果一个失败,其他的仍应尝试。
数据库中已经存在父记录,只有一个字段需要更新,还没有子记录。
问题是 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;
}
解决方案
我的猜测是,传递给函数的 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 并在您进行更改的函数之外完成所有工作。
然后在循环中为从数据库中检索的每个 ParentTable 创建一个新的 Context 实例,进行更改,调用 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
}
}
推荐阅读
- database - 无法连接到 PgBouncer 管理控制台
- html - 悬停效果适用于页面中的所有锚标签
- python - sns.scatter plot python,指定大小
- django - Apache2 上的 Django 应用程序禁止 403 响应
- ios - 在不同部分的单元格中添加多个复选标记并将其状态保存在swift ios中
- javascript - 将数组发布到 ASP NET MVC 控制器
- excel - 无法从 Selenium VBA 中的下拉“日期选择器”(YYYY/MM/DD)中选择日历日期
- arrays - 我有一个 mongodb 查询,我想找到完全匹配的单词“Approved”
- symfony - 如何将两个单选按钮(RadioType)与 Symfony 5 链接?
- python - 如何从 python 列表中获取 5 个最接近的整数?