EF Core“另一个实例已经被跟踪”

32

我在使用EF Core 2.2.3更新.Net Core 2.2.0中的实体时遇到了问题。

保存更改时出错。错误详情:因为存在另一个具有相同 {'Id'} 键值对的实例正在被跟踪,所以无法跟踪实体类型 'Asset' 的实例。在附加现有实体时,请确保只附加一个具有给定键值的实体实例。考虑使用

以下是DB上下文的注册方式:

services.AddDbContext(options =>

options.UseSqlServer(Configuration.GetConnectionString("DbConnection")), ServiceLifetime.Scoped);

默认情况下设置了Scoped生命周期,但我将它写成更易于理解的形式。

可以通过以下方式获取Anomaly对象:

public IQueryable<Anomaly> GetAll()
    {return _context.Anomalies.Include(a => a.Asset).Include(a => a.Level)
}

public async Task<Anomaly> GetAnomaly(int anomalyId, User user)
{
    var anomaly = await GetAll()
        .FirstOrDefaultAsync(a => a.Id == anomalyId);

    return anomaly;
}

Update() 方法看起来像这样:

using (var transaction = _context.Database.BeginTransaction())
{
    try
    {
        _context.Anomalies.Update(anomaly);
        _context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        throw;
    }
}

在此事务之前有一些检查,但在这种情况下没有足够相关的检查。

这就是为什么我会得到实例已经被跟踪的错误。我不明白这是如何发生的...如果上下文是Scoped,那么

..."服务的新实例将为每个范围创建",在这种情况下,为每个请求创建

如果我的PUT请求上下文与GET请求的上下文不同,那么实体是如何被跟踪的?这在最基本的层面上是如何工作的?

唯一让它工作的方法是将所有来自ChangeTracker的条目状态设置为EntityState.Detached。然后它可以工作...但至少根据我的当前知识,这毫无意义..

我发现这个问题,但没有有效的答案,只有关于EF如何进行跟踪的解决方法和假设。


更新 这里是一个带有重新创建此问题的示例的bitbucket链接:EF Core Update Sample

我对从上下文检索的对象进行了序列化。

左边有跟踪 <====> 右边没有跟踪With Tracking on the LEFT <====> With NO tracking on the RIGHT


1
每个请求中存在多个上下文实例。正如您所提到的,使用Detach在这里不是一个有效的解决方案,您需要找到根本原因。您是如何注入_context类型的? - ilkerkaran
“在这个交易之前,它包含一些检查,但在这种情况下没有相关的检查。” 在调用“_context.Anomalies.Update(anomaly);”之前,您确定“_context.Set<Asset>().Local.Count == 0”是正确的吗? - Ivan Stoev
@Ivan,看起来_context.Set<Asset>().Local.Count == 0false。我最终注释掉了该检查之前的所有代码,但它仍然是false... - Marian Simonca
@ilkerkaran,我会在构造函数中这样注入_contextpublic AnomalyManager(SAMSDbContext context){_context = context;}其中_context是一个private readonly字段。在我最初发布的问题中,我提到了如何在Startup.cs文件中注册DbContext。 - Marian Simonca
6个回答

28

默认情况下,当您检索实体时,它们会被跟踪,因此您可以只调用SaveChanges而不调用Update。您也可以通过使用.AsNoTracking()来检索不进行跟踪的实体。

如果尚未跟踪,则需要调用Update,因此如果您使用AsNoTracking,则确实需要在SaveChanges之前使用Update。

public IQueryable<Anomaly> GetAll()
{    return _context.Anomalies
    .Include(a => a.Asset)
    .Include(a => a.Level);
}

public async Task<Anomaly> GetAnomaly(int anomalyId, User user)
{
    var anomaly = await GetAll()
        .AsNoTracking()
        .FirstOrDefaultAsync(a => a.Id == anomalyId);

    return anomaly;
}

你也可以检查实体是否被跟踪,以确定是否需要调用Update:

using (var transaction = _context.Database.BeginTransaction())
{
    try
    {

        bool tracking = _context.ChangeTracker.Entries<Anomaly>().Any(x => x.Entity.Id == anomaly.Id);
        if (!tracking)
        {
            _context.Anomalies.Update(anomaly);
        }

        _context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        throw;
    }
}

1
我尝试了这个,但不起作用。即使在 Include() 之前加上 .AsNoTracking(),甚至在之后加上它,结果仍然相同。 - Marian Simonca
很奇怪,它在我的电脑上可以运行。我已经更新了我的答案,展示了如何检测实体是否被跟踪,这可以用来决定是否调用Update。 - Joe Audette
1
我检查了那个问题,但对我没有用。我不确定是因为.NET Core还是其他原因。我也尝试使用QueryTrackingBehavior字段,但没有改善。我开始相信问题出在另一个层面,可能是我的设置有问题,或者我的配置有问题之类的。如果我发现什么,我会及时更新的。 - Marian Simonca
1
我知道Joe,我已经删除了Update调用,但是没有成功。如果没有它,任何更改都无法到达数据库。 - Marian Simonca
根据我的经验,检查条目是否被跟踪是有风险的,被跟踪的实体和修改后的实体可能不同。但是ID检查会返回true。我猜这种情况发生在我使用AsNoTracking查询数据库时,它返回已经被跟踪的实体但是是新实例,我错了吗? - mkb
显示剩余3条评论

7
我知道我来晚了,但是我遇到了同样的问题。我可以创建实体,但在更新实体时,第一次成功后每次都失败。所以我尝试了以上所有建议,但都没有成功。我在文档中找到了一个方法,一旦我调用了 DbContext.SaveChanges() 方法,就可以调用另一个方法 DbContext.ChangeTracker.Clear()。这个方法应该清除所有被跟踪的实体的更改跟踪器,以便您可以进行未来的更新。希望这能帮助那些遇到更新实体问题的人。 ChangeTracker.Clear() 文档

7

我曾经不小心尝试更新相同的数据,却没有注意到。花了我十个小时才意识到这一点。如果你有和我一样的重复数值,我建议删除那些数据……阅读这篇答案的人可能已经像我一样尝试了互联网上所有的解决方案,毁掉了项目,遇到了相同的错误,并且像我一样省略了重复的数据。朋友,你并不孤单。

model.GroupBy(gb => gb.ID).Select(s=>s.First()).ToList();//remove duplicates!!!!!

好的建议,谢谢!看起来这将是一个可能解决我们在某些时候都会面临的问题的解决方案集合。 - Marian Simonca

1

我遇到了同样的问题,但是尝试向同一实体添加第二行。在我的情况下,这是因为我假设主键是Int Identity并自动生成的。 由于我没有给它分配任何值,第二行具有相同的Id,这是零默认值。 吸取教训,当您在EF的Add()操作期间看到此错误时,请确保您的键是IDENTITY(如果这是您的意图)。


谢谢Javier,就是这样了。我忘了声明一个身份列。 - undefined

0

最终,我们使用了自定义的UpdateEntity方法来保存一些实体上的更改。该方法遍历实体的每个属性、导航属性和集合属性,确保没有链,并在单个时间更新对象。

它适用于大型对象,我们只在这些情况下使用它。对于简单操作,我们仍然使用简单的Update。

这是一个Bitbucket存储库的链接,其中包含源代码

为了使用此方法,您需要首先获取db实体。然后使用db实体和请求接收到的实体调用UpdateEntity方法。

希望它能像帮助我一样帮助你。干杯!


这个解决方案看起来很棒。我在哪里可以获取类中引用的项目(Atkins.OGSDT.MyJCR.Infrastructure.*)? - jim tollan

0
在我的情况下,问题出在在更新1:n关系中的列表时,类定义中的实体缺失。
public class Exercise
{
  public Guid Id { get; set; } 
  //other props...
  public List<EntityInExercise> Entities { get; set; } = new();
}

public class EntityInExercise
{
  public Guid Id { get; set; } 
  //other props...

  //this line was missing 
  public Exercise Exercise{ get; set; }= null!;
  //☝️
}

在这两种情况下(有缺失行和没有缺失行),它都会创建相同的数据库。我遇到的唯一问题是尝试使用Automapper和其Collections更新该列表时。

这并不明显会导致关于重复主键的错误。引用从来都不是主键的一部分。 - Gert Arnold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接