C# 不使用AsNoTracking更新嵌套列表
简化模型:C# 不使用AsNoTracking更新嵌套列表,c#,entity-framework,entity-framework-core,C#,Entity Framework,Entity Framework Core,简化模型: Profile {Guid Id, string Name, List<Property> Properties} Property {Guid Id, string Name, List<Type> Types} Type {Guid Id, string Key, string Value} 在更新服务中: public async Task<Response> Update([FromBody] Profile profile) { var
Profile {Guid Id, string Name, List<Property> Properties}
Property {Guid Id, string Name, List<Type> Types}
Type {Guid Id, string Key, string Value}
在更新服务中:
public async Task<Response> Update([FromBody] Profile profile)
{
var entity = await _context.Profiles
.Include(x => x.Properties)
.ThenInclude(x => x.Types)
.FirstOrDefaultAsync(x => x.Id == profile.Id);
foreach (var prop in profile.Properties)
{
var existingProp = entity.Properties.SingleOrDefault(a => a.Id == prop.Id);
//Update
if (existingProp != null)
{
var entry = _context.Entry(existingProp);
entry.State = EntityState.Modified;
existingProp.ChargeFrom(prop);//maps the new values to the db entity
_context.SaveChanges();
}
}
}
问题解决了,但我不知道为什么会抛出这个异常,其他一些线程提到DbContext可能被另一个进程使用,并且可能注册为Singleton,但在我的例子中,它注册为scoped
我将类型实体标记为Notracking:
。然后包括(x=>x.Types)。AsNoTracking()
问题解决了,但我不知道为什么会抛出这个异常
错误的原因将是因为此行:
existingProp.ChargeFrom(prop);//maps the new values to the db entity
。。。将尝试将未跟踪的类型从prop
复制到existingProp
。使用AsNoTracking
将删除异常,但很可能会导致SaveChanges
上的数据重复,其中Type
将使用标识键或重复行异常进行设置。如果您没有收到任何异常,我将检查Types集合,看看是否有重复的行出现在那里
将数据从未跟踪的实体复制到跟踪的实体时,您需要确保只复制值,而不复制引用。复制一个未跟踪的引用,默认情况下EF会将其视为一个新实体。即使强制将其状态切换到Modified
,DbContext也可能已经在跟踪具有该ID的实体
如果Property.Types是引用的集合,例如与查找的关联,并且这些引用可能会更改添加和删除关联的位置,则要应用更改,需要从数据库加载关联的类型,然后使用该类型删除不再有效的关联,并添加当前未关联的关联
例如:给定一个类型为(Type1)和(Type2)的属性(PropertyA),如果我们将其编辑为具有(Type1)和(Type3),则需要从DbContext(跟踪)中获取Type1和Type3,然后与跟踪的属性进行比较,以确定删除Type2并添加Type3
var entity = await _context.Profiles
.Include(x => x.Properties)
.ThenInclude(x => x.Types)
.SingleAsync(x => x.Id == profile.Id);
// Get the IDs for all Types we want to associate... In the above example this would
// ask for Type1 and Type3 if only the one property. We get a Distinct list because
// multiple properties might reference the same TypeId(s).
var existingTypeIds = profile.Properties
.SelectMany(x => x.Types.Select(t => t.Id))
.Distinct()
.ToList();
// Load references to all Types that will be needed. Where associating new types, these will be referenced.
var existingTypes = _context.Types
.Where(x => existingTypeIds.Contains(x.Id))
.ToList();
foreach (var prop in profile.Properties)
{
existingProp = entity.Properties.SingleOrDefault(x => x.Id == prop.Id);
if (existingProp == null)
continue;
var updatedTypeIds = prop.Types.Select(x => x.Id).ToList();
var existingTypeIds = existingProp.Types.Select(x => x.Id).ToList();
var addedTypeIds = updatedTypeIds.Except(existingTypeIds).ToList();
var removedTypeIds = existingTypeIds.Except(updatedTypeIds).ToList();
var addedTypes = existingTypes
.Where(x => addedTypeIds.Contains(x.Id))
.ToList();
var removedTypes = existingProp.Types
.Where(x => removedTypeIds.Contains(x.Id))
.ToList();
foreach(var removedType in removedTypes)
existingProp.Types.Remove(removedType);
foreach(var addedType in addedTypes)
existingProp.Types.Add(addedType);
}
如果该类型是包含可更新属性的子行,则应在更新数据和现有数据状态之间复制这些值。这增加了大量的工作,尽管像AutoMapper这样的工具可以配置来提供帮助。您仍然需要管理可以添加、删除类型或更改内容的情况。这也适用于属性,因为您的示例只处理属性被更新的情况,而不是潜在地添加或删除的情况
最终,尝试将更新场景组织为尽可能原子化是有益的,以避免对实体、属性和类型的整个对象图进行更改的更新,而不是仅对实体值进行一次更新,对属性值进行一次更新,对单一类型更新进行一次更新。这也适用于添加属性、添加类型、删除属性和删除类型。虽然看起来像是更多的代码来分解这样的操作,但它使它们非常简单和直接,而不是一个大而复杂的方法试图比较之前和之后,以确定要添加、删除和更新的内容。错误隐藏在复杂的代码中,而不是简单的方法中。:)
编辑对象图时,还应避免多次调用SaveChanges
。它不应该在属性的循环中调用,而应该在循环完成时执行一次。这样做的原因是,其中一个属性上的异常会导致持久化的数据状态不完整/无效。如果要保存的对象中有4个属性,而第3个属性因任何原因出现异常而失败,则前2个属性将被更新,最后两个属性不会持久化。通常,在更新操作中,更新应该遵循“要么全有,要么全无”的持久性方法
希望这有助于解释你所看到的行为,并给你一些考虑前进的东西。摆脱无用的代码>条目变量和<代码>条目。状态< /代码>修改和问题解决。code>existingProp已经被EF跟踪,您不必手动添加它来跟踪
Value
属性在没有它的情况下不会更新,或者父对象(entity
)应该被修改
,感谢您的完整回答,实际上我不希望如此复杂!我要求更新,因为我有问题。我可以用完整代码(属性(添加/删除/更新)、类型(添加/删除/更新))更新您的答案吗?
.ThenInclude(x => x.Types).AsNoTracking()
existingProp.ChargeFrom(prop);//maps the new values to the db entity
var entity = await _context.Profiles
.Include(x => x.Properties)
.ThenInclude(x => x.Types)
.SingleAsync(x => x.Id == profile.Id);
// Get the IDs for all Types we want to associate... In the above example this would
// ask for Type1 and Type3 if only the one property. We get a Distinct list because
// multiple properties might reference the same TypeId(s).
var existingTypeIds = profile.Properties
.SelectMany(x => x.Types.Select(t => t.Id))
.Distinct()
.ToList();
// Load references to all Types that will be needed. Where associating new types, these will be referenced.
var existingTypes = _context.Types
.Where(x => existingTypeIds.Contains(x.Id))
.ToList();
foreach (var prop in profile.Properties)
{
existingProp = entity.Properties.SingleOrDefault(x => x.Id == prop.Id);
if (existingProp == null)
continue;
var updatedTypeIds = prop.Types.Select(x => x.Id).ToList();
var existingTypeIds = existingProp.Types.Select(x => x.Id).ToList();
var addedTypeIds = updatedTypeIds.Except(existingTypeIds).ToList();
var removedTypeIds = existingTypeIds.Except(updatedTypeIds).ToList();
var addedTypes = existingTypes
.Where(x => addedTypeIds.Contains(x.Id))
.ToList();
var removedTypes = existingProp.Types
.Where(x => removedTypeIds.Contains(x.Id))
.ToList();
foreach(var removedType in removedTypes)
existingProp.Types.Remove(removedType);
foreach(var addedType in addedTypes)
existingProp.Types.Add(addedType);
}