The instance of entity type cannot be tracked because another instance with this id = { "2"} of this type with the same key is already being tracked

huangapple go评论83阅读模式
英文:

The instance of entity type cannot be tracked because another instance with this id = { "2"} of this type with the same key is already being tracked

问题

这是我的代码,当我想要更新我的FeatureDisa实体时,我收到了这个错误,我不知道为什么会发生这种情况。还有一点是,我不能使用AsNoTracking(),因为我需要记录一些东西。

var updateFeatureDisas = await _featureDisaRepository.
                GetAll()
                .Where(a => featureDisaIdInsert.Contains(a.DisadvantageId) &&
                            featureIdInsert.Contains(a.FeatureId) &&
                            featureParentIdInsert.Contains(a.ParentFeatureId)).ToListAsync();

var sqInsertFeatureDisa = insertTagDisa.Where(a => updateFeatureDisas.All(d => d.Id != a.Id)).ToList();

if (updateFeatureDisas.Any())
{
    var sqUpdateFeatureDisa = insertTagDisa.Where(a => updateFeatureDisas.
                All(d => d.Id.Equals(a.Id, StringComparison.Ordinal))).ToList();
    var updateFeatureDisa = _mapper.Map<List<FeatureDisa>>(sqUpdateFeatureDisa);
    _featureDisaRepository.UpdateRangeWithOutSaveChangeAsync(updateFeatureDisa);
}

这是我的UpdateWithOutSaveChangeAsync方法:

public void UpdateWithOutSaveChangeAsync(TEntity entity)
{
    DbContext.Update(entity);
}
英文:

this is my code when i want tp update my featureDisa entity i get this error
and i dont know why this happen. and there is one point is that i can not use AsNoTracking() beacuse i need log something

var updateFeatureDisas = await _featureDisaRepository.
                GetAll()
                 .Where(a =&gt; featureDisaIdInsert.Contains(a.DisadvantageId) &amp;&amp;
                 featureIdInsert.Contains(a.FeatureId)
                 &amp;&amp; featureParentIdInsert.Contains(a.ParentFeatureId)).ToListAsync();
    
            var sqInsertFeatureDisa = insertTagDisa.Where(a =&gt; updateFeatureDisas.All(d =&gt; d.Id != a.Id)).ToList();
    
    
    
            if (updateFeatureDisas.Any())
            {
                var sqUpdateFeatureDisa = insertTagDisa.Where(a =&gt; updateFeatureDisas.
                    All(d =&gt; d.Id.Equals(a.Id, StringComparison.Ordinal))).ToList();
                var updateFeatureDisa = _mapper.Map&lt;List&lt;FeatureDisa&gt;&gt;(sqUpdateFeatureDisa);
                _featureDisaRepository.UpdateRangeWithOutSaveChangeAsync(updateFeatureDisa);
            }

and this my UpdateWithOutSaveChangeAsync

public void UpdateWithOutSaveChangeAsync(TEntity entity)
{
    DbContext.Update(entity);
}

答案1

得分: 2

EF变更跟踪与引用一起工作。当你从DbContext加载一个实体时,默认情况下EF会跟踪该引用。如果你稍后创建另一个具有相同ID的实体副本并告诉DbContext使用它来Update行,EF首先检查其跟踪列表,如果发现已经在跟踪另一个具有相同ID的实体副本,则会引发此错误。

所以这段代码:

var updateFeatureDisa = _mapper.Map&lt;List&lt;FeatureDisa&gt;&gt;(sqUpdateFeatureDisa);

... 是你的问题。Automapper正在构造新的FeatureDisa实体实例,而你尝试告诉EF更新行,但你已经读取并跟踪了实体。

解决方案1:在加载特性时告诉EF不要跟踪引用。如果你的存储库方法返回一个IQueryable,并且你已经做得很好,查询在存储库中还没有被实例化,那么:

var updateFeatureDisas = await _featureDisaRepository.GetAll()
    .AsNoTracking() // &lt;- 只需添加这一行...
    .Where(a =&gt; featureDisaIdInsert.Contains(a.DisadvantageId) 
        &amp;&amp; featureIdInsert.Contains(a.FeatureId)
        &amp;&amp; featureParentIdInsert.Contains(a.ParentFeatureId))
    .ToListAsync();

应该可以工作,但需要注意一个重要的警告。当处理注入的DbContext时,这一语句将加载所请求的FeatureDisas,而不将它们添加到跟踪缓存中,但在该DbContext(即请求)的生命周期内可能会有其他代码/调用加载和跟踪一个或多个FeatureDisa实体。如果没有小心和意识到这种依赖关系,可能会导致偶发性异常或破坏更改的风险。

选项1a:为了确保上述代码是“安全的”,我们需要确保在调用“Update”之前,DbContext不会跟踪任何实例。如果有的话,告诉它将其分离。很想在你的UpdateWithoutSaveChangesAsync方法中这样做,但由于它是通用方法,这不起作用,因为我们需要通过ID在跟踪缓存中找到任何现有的项。

public void UpdateWithOutSaveChangeAsync(TEntity entity)
{
    var existingEntity = DbContext.DbSet&lt;TEntity&gt;().Local.FirstOrDefault(x =&gt; x.Id == entity.Id); // 由于通用实现而无法使用
    if (existingEntity != null)
        dbContext.Entry(existingEntity).State = EntityState.Detached;
    DbContext.Update(entity);
}

不幸的是,该代码在通用情况下不起作用,除非我们有一个可以访问ID的公共基本类型来进行可能的转换。所以在调用Update之前,必须在FeatureDisaRepository.UpdateRangeWithOutSaveChangeAsync()方法中执行。

假设UpdateRangeWithoutSaveChangeAsync本身是通用实现,类似于:

public async Task UpdateRangeWithOutSaveChangeAsync(List&lt;FeatureDisa&gt; featureDisas)
{
    var featureDisaIds = featureDisas.Select(x =&gt; x.Id).ToList();
    var trackedFeatureDisas = DbContext.FeatureDisas.Local
        .Where(x =&gt; featureDisaIds.Contains(x.Id))
        .ToList();

    foreach(var trackedFeatureDisa in trackedFeatureDisas)
        DbContext.Entry(trackedFeatureDisa).State = EntityState.Detached;

    return base.UpdateRangeWithOutSaveChangeAsync(featureDisas); // 交给通用方法来完成工作。
}

这基本上是我脑海中提取出来的,根据你的存储库是如何实现的,由于通用模式的原因,可能会变得更加混乱。(我真的不建议在EF中使用通用存储库,原因就像这样)

选项2:幸运的是,有比所有这些更好的选择,Automapper可以实现。Automapper可以在引用之间复制值,因此在原始读取调用加载跟踪引用的情况下,而不是:

var updateFeatureDisa = _mapper.Map&lt;List&lt;FeatureDisa&gt;&gt;(sqUpdateFeatureDisa);

使用Automapper的复制映射方法调用:

foreach(var updated in sqUpdateFeatureDisa)
{
    var existingFeatureDisa = updateFeatureDisas.FirstOrDefault(x =&gt; x.Id = updated.Id);
    if (existingFeatureDisa == null) continue; // 如果没有匹配的现有行,还能做什么?
    _mapper.Map(updated, existingFeatureDisa); // 从DTO复制值到跟踪的实体。
}

不需要在存储库中调用“Update”方法,唯一需要做的就是等待一个DbContext.SaveChanges()调用,修改后的FeatureDisa实例将被持久化。请注意,在这个选项中,我们希望在获取updateFeatureDisa集合的初始查询中添加AsNoTracking()。我们需要跟踪的引用,以便让EF执行它的工作。

Automapper在复制Mapper.Map调用周围的文档一直都相当缺乏。对于这样的更新操作,它非常有用。

你可以添加到这个选项的唯一其他细节是,在设置来自导入DTO到FeatureDisa映射的Automapper配置时,你可以配置它只复制你打算允许更改的值。目前,你正在使用映射器从DTO构建一个新的FeatureDisa,但由于我们正在更新跟踪的加载记录,你可以更加悲观地保护数据,以仅允许你打算更新的值,以保护数据免受潜在的篡改数据的威胁。这意味着你的更新DTO可以

英文:

EF change tracking works with references. When you load an entity from a DbContext, by default EF tracks that reference. If you later create another copy of an entity with the same ID and tell the DbContext to Update the row with it, EF first checks its tracking list and throws this error if it finds that it is already tracking another copy of the entity with the same ID.

So this code:

var updateFeatureDisa = _mapper.Map&lt;List&lt;FeatureDisa&gt;&gt;(sqUpdateFeatureDisa);

... is your problem. Automapper is constructing new FeatureDisa entity instances which you are trying to tell EF to Update rows, but you have already read and tracked entities.

Solution 1: Tell EF to not track references when loading the Features. If your Repository method is returning an IQueryable and you've done it properly where the query hasn't been materialized in the Repository then:

var updateFeatureDisas = await _featureDisaRepository.GetAll()
    .AsNoTracking() // &lt;- Just add this...
    .Where(a =&gt; featureDisaIdInsert.Contains(a.DisadvantageId) 
        &amp;&amp; featureIdInsert.Contains(a.FeatureId)
        &amp;&amp; featureParentIdInsert.Contains(a.ParentFeatureId))
    .ToListAsync();

This should work, but there is a big caveat to be aware of. When dealing with injected DbContexts, this one statement will load the requested FeatureDisas without adding them to the tracking cache, but it may be possible that other code/calls within the lifetime scope of that DbContext (I.e. Request) might load and track one or more FeatureDisa entities. It is certainly a risk of an intermittent exception or breaking change if someone is not careful and aware of this dependency.

Option 1a: To ensure that the above code is "safe", we need to ensure that before calling "Update" the DbContext is not tracking any instance. If it is, tell it to Detach it. It would be nice to do this in your UpdateWithoutSaveChangesAsync method, but as a Generic method that won't work because we need to find any existing item in the tracking cache by ID.

public void UpdateWithOutSaveChangeAsync(TEntity entity)
{
    var existingEntity = DbContext.DbSet&lt;TEntity&gt;().Local.FirstOrDefault(x =&gt; x.Id == entity.Id); // Doesn&#39;t work due to Generic implementation
    if (existingEntity != null)
        dbContext.Entry(existingEntity).State = EntityState.Detached;
    DbContext.Update(entity);
}

Unfortunately that code won't work because we cannot do that in a Generic unless we have a common base type to access the ID that we can possibly cast to. So it will have to be done in the FeatureDisaRepository .UpdateRangeWithOutSaveChangeAsync() method before it calls that Update:

Assuming UpdateRangeWithoutSaveChangeAsync is itself a Generic implementation: Something like:

 public async Task UpdateRangeWithOutSaveChangeAsync(List&lt;FeatureDisa&gt; featureDisas)
 {
      var featureDisaIds = featureDisas.Select(x =&gt; x.Id).ToList();
      var trackedFeatureDisas = DbContext.FeatureDisas.Local
           .Where(x =&gt; featureDisaIds.Contains(x.Id))
           .ToList();

      foreach(var trackedFeatureDisa in trackedFeatureDisas)
          DbContext.Entry(trackedFeatureDisa).State = EntityState.Detached;

      return base.UpdateRangeWithOutSaveChangeAsync(featureDisas); // hand off to the Generic method to do the work.
}

That's basically pulled from my head, depending on how your repository is implemented it's probably going to get more messy due to the Generic pattern. (I really do not recommend Generic Repositories /w EF for reasons like this)

Option 2: Fortunately there is a better option than all of that which Automapper can facilitate. Automapper can copy values between references, so leaving your original read call loading tracked references, instead of:

 var updateFeatureDisa = _mapper.Map&lt;List&lt;FeatureDisa&gt;&gt;(sqUpdateFeatureDisa);

Use automapper's copy mapping method call:

foreach(var updated in sqUpdateFeatureDisa)
{
    var existingFeatureDisa = updateFeatureDisas.FirstOrDefault(x =&gt; x.Id = updated.Id);
    if (existingFeatureDisa == null) continue; // What else to do if we don&#39;t have a matching existing row?
    _mapper.Map(updated, existingFeatureDisa); // copy values from dto to tracked entity.
}

No need to call an "Update" method in the repository, all that is left is to await a DbContext.SaveChanges() call and the modified FeatureDisa instances will be persisted. Note that in this option we don't want to add AsNoTracking() to the initial query to get the updateFeatureDisa collection. We want tracked references so that we can let EF do it's thing.

Automapper's documentation around that copy Mapper.Map call has been pretty lacking in the past. It's really useful for update actions like this.

The only other detail you can add to this option is that when setting up the Automapper configuration from your import DTO to FeatureDisa mapping, you configure it to only copy across the values you intend to allow to be changed. Currently you are using the mapper to construct a new FeatureDisa from the DTO, but since we are updating tracked, loaded records, you can be more pessimistic to guard the update to only allow values you intend to update to protect the data from potentially tampered data coming in. This means your update DTO can be streamlined to reflect only the values that can/should change as you no longer need to pass all of the fields needed to construct a complete FeatureDisa record to Update.

huangapple
  • 本文由 发表于 2023年6月13日 18:27:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76463947.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定