繁体   English   中英

EF Core 在循环中更新实体和子实体 - 在第一次通过时保存提交整个列表的更改

[英]EF Core update entity and child entities in loop - save changes commiting entire list on first pass

我想更新父表上的一个字段并在 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 中被传递到 function 的“父”表是从 _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 并在您进行更改的 function 之外完成所有工作。

然后在循环中为从数据库中检索的每个 ParentTable 创建一个新的 Context 实例,进行更改,调用 SaveChangesAsync,然后处理上下文。

下一个循环,再做一次。

在 class 模块级别保留 _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
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM