我正在使用 C# 开发一种异步方法,该方法使用 Entity Framework Core 将记录列表插入到 SQL Server 数据库中。该方法旨在将数据批量插入数据库并保存更改。
public async Task<int> PersistDataAsync(List<ExportData> dataList)
{
var groupedData = dataList
.SelectMany(data => data.Data)
.SelectMany(customer => customer.ItemData, (customer, item) => new { customer.CustomerId, Item = item })
.GroupBy(x => x.CustomerId)
.ToDictionary(g => g.Key, g => g.Select(x => x.Item).ToList());
int persistedCount = 0;
foreach (var custId in groupedData.Keys)
{
var connectionString = GetConnectionString(_serviceProvider, custId);
if (connectionString == null)
{
_logger.LogError($"Could not retrieve connection string for customer {custId}");
continue;
}
using (var context = CreateDbContext(connectionString, custId, _configuration.GetValue<int>("DbCommandTimeout")))
{
if (!IsDatabaseConnected(context, custId))
{
continue;
}
IDataRepository dataRepository = new DataRepository(context, _dataLogger);
persistedCount += await dataRepository.PersistData(groupedData[custId].ToList());
}
}
return persistedCount;
}
public async Task<int> PersistDataAsync(
IEnumerable<DataRecord> dataList)
{
var data = dataList.ToList();
return await AppendRecords(data);
}
public async Task<int> AppendRecords(List<YourEntityType> records)
{
await _db.YourEntityType.AddRangeAsync(records);
int recordsAdded = 0;
try
{
recordsAdded = await _db.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "{MethodName} error {ExceptionMessage}",
nameof(AppendRecords),
ex.InnerException?.Message);
}
return recordsAdded;
}
当实体框架尝试跟踪具有相同键值的同一实体类型 (YourEntityType) 的多个实例时,似乎会发生该错误。我怀疑记录列表可能包含重复的实体,或者应用程序的另一部分可能已经在跟踪具有相同密钥的实例。
我们如何解决这个问题?
AddRange
仅适用于非常简单且安全的插入操作。当将 AddRange
与实体一起使用时,这些实体必须是:
第 3 点的意思是,如果我有一个要插入的订单列表,并且订单具有对 OrderStatus 表的 OrderStatus 实体的引用以实现引用完整性,并且我尝试插入状态为“待处理”的订单,我可以结束出现此错误是因为我想将这些新订单关联到现有的“待处理”订单状态,但对每行的 OrderStatus 实体的引用将被取消跟踪,因此它将尝试插入待处理订单状态的行。
为了满足第一点,您需要首先清理输入中的重复项。根据这些数据的来源,如果可能存在重复,请将其删除。
要满足第 2 点,取决于您是否打算仅插入新行,或者更新与插入是否找到现有行。如果您希望执行“更新插入”,那么您需要根据是否找到/填充 ID 将插入与更新分开。更新涉及从
DbContext
获取现有实体并复制值。可以添加插入物。
满足第 3 点可能是最多的工作,具体取决于对象模型的简单或复杂程度。对于可能引用的任何和所有其他实体,您需要在插入主实体之前确保引用与
DbContext
关联。对于引用,可以通过 Attach
或从 DbContext
获取引用的实体来完成。如果使用 Attach
,您仍然需要首先检查跟踪缓存。例如,如果 YourEntityType 有对客户的引用,即数据库中的现有客户记录:
foreach (var record in records)
{
var existingCustomer = _context.Customers.Local.FirstOrDefault(x => x.CustomerId == record.Customer.CustomerId);
if (existingCustomer != null)
record.Customer = existingCustomer;
else
_context.Attach(record.Customer);
_context.YourEntityTypes.Add(record);
}
检查“.Local”将进入跟踪缓存,而不是数据库。如果我们发现现有客户被跟踪,我们将替换引用,因为 record.Customer 可能具有相同的 ID,但它是一个未跟踪的实例,EF 将其视为新的客户记录。如果我们没有跟踪客户,那么我们可以
Attach
它,这将开始跟踪它并将其视为现有的客户记录。如果我们只是调用 Attach
而不首先检查缓存,那么如果没有跟踪的实体,它将起作用,但如果已经跟踪了 Customer 的另一个实例,它将失败并出现异常。只有跟踪所有引用后,我们才能添加记录。