实体框架核心错误:“无法跟踪实体类型的实例,因为另一个实例具有相同的键值”

问题描述 投票:0回答:1

我正在使用 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) 的多个实例时,似乎会发生该错误。我怀疑记录列表可能包含重复的实体,或者应用程序的另一部分可能已经在跟踪具有相同密钥的实例。

我们如何解决这个问题?

c# .net sql-server entity-framework entity-framework-core
1个回答
0
投票

AddRange
仅适用于非常简单且安全的插入操作。当将
AddRange
与实体一起使用时,这些实体必须是:

  1. 独一无二,没有重复。
  2. 数据库中尚不存在。
  3. 简单,不包含对数据库中已有任何记录的引用。

第 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 的另一个实例,它将失败并出现异常。只有跟踪所有引用后,我们才能添加记录。

© www.soinside.com 2019 - 2024. All rights reserved.