从DbContext删除实例会引发重复的ID错误

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

我有一个测试,可以对数据库进行种子处理,然后调用一种方法来加载已种子的对象并将其删除。但是,当该方法调用dbContext.Remove(...)时,出现错误消息,

System.InvalidOperationException:无法跟踪实体类型'FiveWhysAnalysis'的实例,因为已经跟踪了另一个具有相同键值的{'Id'}实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

我的代码看起来像这样...

Seed.cs

public Seed(MyContext dbContext) {
    this.dbContext = dbContext;
}

public Task Seed() {
    this.DataStory = new DataStory(...)
    this.FiveWhyAnalysis = new FiveWhyAnalysis(this.DataStory.Id, ...) // Doesn't touch Id property
    this.dbContext.FiveWhyAnalyses.Add(fivewhy);
    return this.dbContext.SaveChangesAsync();
}

DeleteFiveWhyMutator.cs

public DeleteFiveWhyMutator(MyContext dbContext, int dataStoryId) {
    this.dbContext = dbContext;
    this.dataStoryId = dataStoryId;
}

public async Task Load(MyContext dbContext) {
    DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
    dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);

    // NOTE: I have also tried using an include by doing:
    // DataStory dataStory = await dbContext.DataStories.Include(ds => ds.FiveWhyAnalysis).FirstAsync(ds => ds.Id == this.dataStoryId);
    // rather than the above implementation of this method.
    return dataStory;
}

public async Task<DataStory> Run(MyContext dbContext, DataStory dataStory) {
    dbContext.FiveWhysAnalyses.Remove(dataStory.FiveWhysAnalysis); // Error here
    await dbContext.SaveChangesAsync();
    return dataStory;
}

DeleteFiveWhyMutatorTest.cs

MyContext dbContext = ... // Injected using Microsoft DI
Seed seed = new Seed(dbContext);
await seed.Seed();
var mutator = new DeleteFiveWhyMutator(dbContext, seed.DataStory.Id);
DataStory dataStory = await mutator.Load(dbContext);
await mutator.Run(dbContext);
c# entity-framework invalidoperationexception
1个回答
0
投票

对此示例场景有点感觉不对。例如:

DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);

如果DataStory已经具有FiveWhysAnalysis的导航属性,并且您想获取关联的导航属性:

DataStory dataStory = await dbContext.DataStories.Include(ds => ds.FiveWhysAnalysis).FirstAsync(ds => ds.Id == this.dataStoryId);

正如您提到的那样,请尽早加载它。这是正确的方法。不需要将相关实体作为额外的查询加载并覆盖Navigation属性,这很容易出错。

下一个可能是过度使用异步操作。尽管这些功能非常适合卸载长时间运行的查询,以释放Web服务器以在等待响应时启动其他请求,但通常不应将其默认设置为。调用多个DbContext调用/ w async可能会导致这些调用跨越线程,并且DbContext不是线程安全的。例如,如果您将示例更新为:

public async Task Load(MyContext dbContext) 
{
    Console.WriteLine("Thread @1: " + Thread.CurrentThread.ManagedThreadId);
    DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
    Console.WriteLine("Thread: @2" + Thread.CurrentThread.ManagedThreadId);
    dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);
    Console.WriteLine("Thread: @3" + Thread.CurrentThread.ManagedThreadId);

   //...
}

您将看到类似以下内容的输出:线程@ 1:13线程@ 2:14线程@ 3:15

取决于应用程序的配置运行方式,异步操作可在与调用它们不同的线程上恢复执行。只要等待all

操作,通常就可以。 AFAIK,只要一次是一个线程,DbContext就不会因在另一个线程上继续进行的请求而抛出异常。但是,如果您忘记或忽略了等待操作,则可能会受到跨线程访问的困扰。异步操作也增加了开销,因此它们非常适合仅少量地用于您希望花多一点时间才能执行的查询。默认情况下使用它们会使您的代码总体上变慢。

[接下来,您提到使用依赖注入,尽管您的方法都被构造为接受DbContext,这违背了依赖注入的目的。我还将研究如何对DbContext进行生命周期限制,以确保它不是瞬态的,而是按请求或显式作用域进行作用域。 DbContext仅应在事件的构造函数上需要,并且DI应确保构造的所有类都接收相同的DbContext实例。 (未在方法中传递DbContexts)

您的返回类型也没有意义。您已经返回Task,但是没有使用它,并且当mutator的目的是控制对象实例的操作方式时,您可以使用公共方法来“加载”实体。这种模式的实现并不能阻止某人仅加载实体并使用实体。它具有复杂的代码气味,没有明确的目的。

[我个人认为这些点都不能解释您所看到的行为,但是结合起来,它们可能隐藏了一个错误的假设,导致多个引用链接到已经在跟踪匹配实例的DbContext。我建议首先从实现最简单的事情开始,先验证行为,然后针对要尝试完成的所需模式逐渐重构。首先,删除所有异步操作并修复DI,以便仅在构造函数上初始化DbContext引用。提取实体/ w渴望加载,然后调用您的mutator。

public DeleteFiveWhyMutator(MyContext dbContext, int dataStoryId) {
    this.dbContext = dbContext;
    this.dataStoryId = dataStoryId;
}

private FiveWhyAnalysis GetAnalysis() {
    var fwAnalysis = dbContext.DataStories
       .Where(ds => ds.Id = dataStoryId)
       .Select(ds => ds.FiveWhyAnalysis)
       .SingleOrDefault();
    return fwAnalysis;
}

public void Run() {
    var fwAnalysis = GetAnalysis();
    if (fwAnalysis == null)
        return;

    dbContext.FiveWhysAnalyses.Remove(fwAnalysis);
    dbContext.SaveChanges();
}

然后在测试中:

using (var dbContext = new MyContext()) // Real code will DI the context.
{
    Seed seed = new Seed(dbContext); 
    seed.Seed(); // remove async here as well to test.

    // Assert we have a FiveWhy...
    Assert.IsNotNull(seed.DataStory.FiveWhyAnalysis, "FiveWhyAnalysis was not seeded.");
    var mutator = new DeleteFiveWhyMutator(dbContext, seed.DataStory.Id);
    mutator.Run();

    // Assert the FiveWhy was removed...
    Assert.IsNull(seed.DataStory.FiveWhyAnalysis, "FiveWhyAnalysis was not removed.");
}

如果可以,请从那里展开。总的来说,我建议您谨慎使用这样的模式实现,因为它可能会涉及多个mutator或共享DbContext引用的其他类,它们在不同时间都调用SaveChanges(),这可能导致较大操作的不同阶段提交部分更改。扩展mutator可以包括针对现有DataStory引用的Run方法:

public void Run(DataStory dataStory) {
    if (dataStory == null)
        throw new ArgumentNullException("dataStory");

    if (dataStory.FiveWhyAnalysis == null)
        return;

    dbContext.FiveWhysAnalyses.Remove(dataStory.FiveWhyAnalysis);
    dbContext.SaveChanges();
}

如果您已经加载了数据故事并想要使用增变器来管理删除FiveWhyAnalysis,可以调用此替代方法。为了使该模式具有比直接修改实体状态更大的价值,它需要提供其他价值,例如包装审核,变更跟踪或其他常见行为。

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