无法使用 EF Core 插入具有现有导航属性的新实体

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

我正在导入一个巨大的文件,这使我创建了很多模块,其产品具有 1 对 N 的关系。

但是,可能会出现错误(异常),并且这些异常与产品存在 N 对 N 的关系:

public class Module 
{
    public int Id { get; set; }

    public ICollection<Product> Products { get; set; } = new List<Product>();
}

public class Product 
{
    public int Id { get; set; }

    public int ModuleId { get; set; }
    public Module? Module { get; set; }

    public ICollection<Anomalie> Anomalies { get; set; } = new List<Anomalie>();
}

public class Anomalie
{
    public int Id { get; set; }
    public ICollection<Product> Products { get; set; } = new List<Product>();
}

需要注意的是,异常已经存在于数据库中,我不会创建新的异常,只会将它们链接到产品。

这里我有一个通用的

AddBulkAsync
方法,我可以在其中发送实体以便插入它:

public virtual async Task<IList<T>> AddBulkAsync(ICollection<T> entities)
{
    try
    {
        await _dbContext.Set<T>().AddRangeAsync(entities);
        await _dbContext.SaveChangesAsync();
    
        _logger.LogInformation($"Entité ajoutée avec succès - {typeof(T).Name}");
        return entities.ToList();
    }
    catch (Exception ex)
    {
        _logger.LogError($"Impossible d'ajouter l'entité {typeof(T).Name} : {ex.Message}");
    }
   
    return new List<T>();
}

简单来说,假设我有一个仅插入一个实体的列表:

var modulesToInsert = new List<Module>() {
     new Module() {
        Products = new List<Product>() {
            new Product() { ... },
            new Product() { ... },
            new Product() { ... },
        }
    }
};

await AddBulkAsync(moduleToInsert); // This works

// ----------------------------------------------------------
var anomalie1 = await GetAnomalieWithIdAsync(1); //get from db
var anomalie2 = await GetAnomalieWithIdAsync(2); //get from db

var modulesToInsert2 = new List<Module>() {
     new Module() {
        Products = new List<Product>() {
            new Product() { Anomalies  = new List<Anomalie>() { anomalie1, anomalie2 } },
            new Product() { Anomalies  = new List<Anomalie>() { anomalie1, anomalie2 } },
        }
    }
};

await AddBulkAsync(moduleToInsert2); // This doesn't works

这里我收到一条错误消息,告诉我实体

Anomalie
Id = 1
已被跟踪。

我尝试用

Anomalie
创建一个空对象
Id = 1
,但我得到了同样的错误。

我还需要继续使用

AddRange
,因为我可以插入很多实体,请问有什么建议吗?

c# entity-framework-core many-to-many
1个回答
0
投票

首先,当涉及到 EF 实体时,不要使用像 Generic Repository 这样的 Generic 模式。泛型的工作原理是,它们的行为 100% 独立于它们所包装的类型。应该清楚的是,对于给定的操作,某些类的行为将与其他类不同。某些类需要处理导航属性,而其他类则不需要。我不建议深入使用条件逻辑来尝试寻找通用解决方案来节省几行简单的 EF 代码。

关于对现有实体的引用,是的,这些绝对需要特殊处理。尽管从您的示例来看,我怀疑这实际上并不是您正在运行的代码,因为它可能会按照编写的方式工作。我会看看这个

GetAnomalieWithIdAsync(1)
方法实际上在做什么。要使这样的代码正常工作,您需要确保两件事:

  1. GetAnomalieWithIdAsync
    使用与上面的代码用于插入产品相同的 DbContext 实例。
  2. GetAnomalieWithIdAsync
    不是使用
    AsNoTracking
    或以其他方式分离读取的异常。

当开发人员将分离或反序列化的实体传递到方法中并仅将它们关联到要保存的新产品而不考虑 DbContext 不跟踪这些实例时,更常见的情况是出现此错误。 DbContext 不知道引用的实体是否代表数据库中的现有记录,或者是否应将其视为与要添加的项目关联的新项目,除非它碰巧正在跟踪该实体。例如,它不会根据实体是否具有 PK 集或默认值做出假设。

您希望确保,如果您在多个产品中引用相同的现有实体实例,则所有产品都使用对该实体的完全相同的引用。例如:

// won't work:
products.Add(new Product { Anomalies = new[] { New Anomalie { Id = 1 } }.ToList());
products.Add(new Product { Anomalies = new[] { New Anomalie { Id = 1 } }.ToList());

// better:
var anomalie = new Anomalie { Id = 1 };
products.Add(new Product { Anomalies = new [] { anomalie });
products.Add(new Product { Anomalies = new [] { anomalie });

但是,由于该异常代表现有记录,因此我们需要确保 DbContext 正在跟踪它。这意味着从我们用来插入这些产品的 DbContext 实例加载它:

var anomalie = await _dbContext.Anomalies.SingleAsync(x => x.Id == 1);
product.Anomaies.Add(anomalie);    

...或者关联一个“存根”或分离的实例,只要 DbContext 尚未跟踪引用。例如,如果我们的方法传入一个独立的 Anomalie 引用:

var existingAnomalie = _dbContext.Anomalies.Local.FirstOrDefault(x => x.Id == anomalie.Id);
if (existingAnomalie != null)
    anomalie = existingAnomalie;
else
    _dbContext.Attach(anomalie);

product.Anomaies.Add(anomalie);    

基本上,每当给定一个分离的引用时,我们都会转到

.Local
跟踪缓存以查看 DbContext 是否正在跟踪引用。如果是这样,我们应该使用该参考。如果没有,那么我们可以附上此参考并使用它。

一个重要提示:当您有一个具有基于集合的导航属性的类时,请使 setter

protected
以避免出现如下代码:

product.Anomalies = new List()

构建新产品时,类似的东西是无害的,但如果将其用于现有的产品实体引用,则会导致 EF 出现问题。

public class Product 
{
    public int Id { get; set; }

    public int ModuleId { get; set; }
    [Required]
    public Module? Module { get; set; }

    public ICollection<Anomalie> Anomalies { get; protected set; } = new List<Anomalie>();
}

我建议使用构造函数或工厂方法来构建实体,特别是当它们具有所需的关系时。您可以拥有一个

protected
默认构造函数来让 EF 满意。

public Product(Module module, params Anomalie[] anomalies)
{
    Module = module;
    ModuleId = module.Id;
    Anomalies = anomalies.ToList();
}

// Available for EF
protected Product() {}

如果使用这样的构造函数来传递 Anomalie 引用,您首先需要确保 DbContext 能够愉快地跟踪它们,以避免像您遇到的错误。

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