如何克隆SnapshotCompaniesHouseSearchResults并保存关联子表新父ID?
解决克隆父表及关联子表数据并关联新父项ID的问题
问题原因
当前代码仅修改了父表实体的JobId并批量插入,但存在以下核心问题:
- 直接操作上下文跟踪的原实体,EF Core可能误将其识别为更新操作而非新增
- 未处理关联子表:子实体仍绑定原父实体主键,未生成新副本插入数据库
- 未建立新父实体与新子实体的外键关联
解决方案步骤
- 查询原数据时禁用上下文跟踪,避免干扰新实体插入
- 为每个原父实体创建全新副本,设置新
JobId及主键(根据主键生成策略处理) - 为每个原子实体创建全新副本,关联新父实体的主键
- 先批量插入父表数据,再批量插入子表数据(若父主键为数据库生成,需配置批量插入返回新主键)
修改后的代码实现
克隆方法更新
private async Task CopySnapshotCompanyHouseData(Guid jobIdOld, Guid jobIdNew) { // 禁用上下文跟踪,避免原实体干扰新实体插入逻辑 var originalData = await _dbContext.SnapshotCompaniesHouseSearchResults .Where(x => x.JobId == jobIdOld) .Include(x => x.SnapshotCompaniesHousePreviousNames) .AsNoTracking() .ToListAsync(); var newParents = new List<SnapshotCompaniesHouseSearchResults>(); var newChildren = new List<SnapshotCompaniesHousePreviousNames>(); foreach (var originalParent in originalData) { // 创建新父实体副本,复制非主键、非关联集合的属性 var newParent = new SnapshotCompaniesHouseSearchResults { JobId = jobIdNew, CompanyName = originalParent.CompanyName, RegistrationNumber = originalParent.RegistrationNumber, // ... 补充其他需要克隆的父实体属性 // 若主键为客户端生成(如Guid),生成新主键 Id = Guid.NewGuid() // 若主键为数据库自增(如int),无需设置,插入后由EF自动填充 }; newParents.Add(newParent); // 处理关联子实体,创建副本并绑定新父实体主键 foreach (var originalChild in originalParent.SnapshotCompaniesHousePreviousNames) { var newChild = new SnapshotCompaniesHousePreviousNames { PreviousName = originalChild.PreviousName, ChangeDate = originalChild.ChangeDate, // ... 补充其他需要克隆的子实体属性 // 关联新父实体的主键 SnapshotCompaniesHouseSearchResultId = newParent.Id, // 若子实体主键为客户端生成,生成新主键 Id = Guid.NewGuid() }; newChildren.Add(newChild); } } // 先插入父表,确保子表能获取到有效父主键 await _dbContext.AbleBulkInsertAsync(newParents); await _dbContext.AbleBulkInsertAsync(newChildren); }
批量插入扩展方法更新(针对数据库生成主键场景)
如果父实体主键是数据库自增类型(如int),需修改批量插入配置,启用返回插入后的主键值,确保子实体能正确关联:
public static async Task AbleBulkInsertAsync<T>(this DbContext dbContext, List<T> entities, int pageSize = 1000) where T : class { int pageNumber = 0; while (true) { var page = entities.Skip(pageNumber * pageSize).Take(pageSize).ToList(); if (page.Count == 0) return; // 启用SetOutputIdentity,让EF返回插入后的主键值 await dbContext.BulkInsertAsync(page, new BulkConfig { BulkCopyTimeout = 0, SetOutputIdentity = true }); pageNumber++; } }
关键注意点
- 禁用跟踪:必须使用
AsNoTracking(),避免原实体被上下文跟踪导致插入冲突 - 实体副本:必须创建全新实体对象,不能直接修改原查询实体,确保EF识别为新增操作
- 外键绑定:子实体的外键必须指向新父实体的主键,而非原父实体
- 主键策略:根据实体主键生成方式(客户端/数据库)调整主键赋值逻辑
内容的提问来源于stack exchange,提问作者Eugene Sukh
相关产品推荐
相关产品推荐

