EF Core - adding/updating 实体和 adding/updating/在一个请求中删除子实体
EF Core - adding/updating entity and adding/updating/removing child entities in one request
我正在为一些看似基本的操作而苦苦挣扎。
假设我有一个 class 名为 Master:
public class Master
{
public Master()
{
Children = new List<Child>();
}
public int Id { get; set; }
public string SomeProperty { get; set; }
[ForeignKey("SuperMasterId")]
public SuperMaster SuperMaster { get; set; }
public int SuperMasterId { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string SomeDescription { get; set; }
public decimal Count{ get; set; }
[ForeignKey("RelatedEntityId")]
public RelatedEntity RelatedEntity { get; set; }
public int RelatedEntityId { get; set; }
[ForeignKey("MasterId")]
public Master Master { get; set; }
public int MasterId { get; set; }
}
我们有一个这样的控制器动作:
public async Task<OutputDto> Update(UpdateDto updateInput)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update properties
entity.SomeProperty = "Updated value";
entity.SuperMaster.Id = updateInput.SuperMaster.Id;
foreach (var child in input.Children)
{
if (entity.Children.All(x => x.Id != child.Id))
{
// This input child doesn't exist in entity.Children -- add it
// Mapper.Map uses AutoMapper to map from the input DTO to entity
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// The input child exists in entity.Children -- update it
var oldChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (oldChild == null)
{
continue;
}
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
}
foreach (var child in entity.Children.Where(x => x.Id != 0).ToList())
{
if (input.Children.All(x => x.Id != child.Id))
{
// The child doesn't exist in input anymore, mark it for deletion
child.Id = -1;
}
}
entity = await _masterRepository.UpdateAsync(entity);
// Use AutoMapper to map from entity to DTO
return MapToEntityDto(entity);
}
现在的存储库方法(MasterRepository):
public async Task<Master> UpdateAsync(Master entity)
{
var superMasterId = entity.SuperMaster.Id;
// Make sure SuperMaster properties are updated in case the superMasterId is changed
entity.SuperMaster = await Context.SuperMasters
.FirstOrDefaultAsync(x => x.Id == superMasterId);
// New and updated children, skip deleted
foreach (var child in entity.Children.Where(x => x.Id != -1))
{
await _childRepo.InsertOrUpdateAsync(child);
}
// Handle deleted children
foreach (var child in entity.Children.Where(x => x.Id == -1))
{
await _childRepo.DeleteAsync(child);
entity.Children.Remove(child);
}
return entity;
}
最后,来自ChildrenRepository的相关代码:
public async Task<Child> InsertOrUpdateAsync(Child entity)
{
if (entity.Id == 0)
{
return await InsertAsync(entity, parent);
}
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
// We have already updated child properties in the controller method
// and it's expected that changed entities are marked as changed in EF change tracker
return entity;
}
public async Task<Child> InsertAsync(Child entity)
{
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
entity = Context.Set<Child>().Add(entity).Entity;
// We need the entity Id, hence the call to SaveChanges
await Context.SaveChangesAsync();
return entity;
}
Context
属性 实际上是 DbContext
并且事务是在操作过滤器中启动的。如果操作抛出异常,则操作过滤器执行回滚,如果没有,则调用 SaveChanges。
发送的输入对象如下所示:
{
"someProperty": "Some property",
"superMaster": {
"name": "SuperMaster name",
"id": 1
},
"children": [
{
"relatedEntity": {
"name": "RelatedEntity name",
"someOtherProp": 20,
"id": 1
},
"count": 20,
"someDescription": "Something"
}],
"id": 10
}
Masters
table 当前有一个 ID 为 10 的记录。它没有子项。
抛出的异常是:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
这是怎么回事?我认为 EF 应该跟踪更改,包括知道我们在该内部方法中调用了 SaveChanges。
EDIT 删除对 SaveChanges 的调用没有任何改变。此外,在观察 SQL Server Profiler 中发生的情况时,我找不到任何由 EF 生成的 INSERT 或 UPDATE SQL 语句。
EDIT2调用SaveChanges时有INSERT语句,但仍然没有Master实体的UPDATE语句
像往常一样,将这个问题发布到 Whosebug 帮助我解决了问题。代码本来不像上面的问题,但是我在写问题的同时修复了代码。
在写这个问题之前,我花了将近一天的时间试图找出问题所在,所以我尝试了不同的东西,比如重新创建实体实例并手动附加它们,将一些实体标记为 Unchanged/Modified,使用 AsNoTracking 甚至完全禁用所有实体的自动更改跟踪并手动标记所有实体添加或修改。
原来导致这个问题的代码是在 child 存储库的私有方法中,我认为它不相关,所以我省略了它。不过,如果我没有忘记从中删除一些手动更改跟踪代码,它确实不相关,这些代码基本上会干扰 EF 的自动更改跟踪器并导致其行为异常。
但是,感谢 Whosebug,问题得到了解决。当你和别人谈论这个问题时,你需要自己 re-analyze 来解释它的所有细节,以便与你交谈的人(在这种情况下,SO 社区)能够理解它。当你 re-analyze 它时,你会注意到所有小 problem-causing 位,然后更容易诊断问题。
所以无论如何,如果有人因为标题而被这个问题吸引,通过 google 搜索或 w/e,这里有一些要点:
如果您要更新多个级别的实体,请始终调用 .Include
以在获取现有实体时包含所有相关的导航属性。这将使它们全部加载到更改跟踪器中,您将不需要手动 attach/mark。完成更新后,调用 SaveChanges 将正确保存所有更改。
当您需要更新 child 实体时,不要对 top-level 实体使用 AutoMapper,尤其是当您必须在更新 child 时实现一些额外的逻辑时]ren.
永远不要像我在将 Id 设置为 -1 时尝试的那样更新主键,或者像我在控制器更新方法的这一行中尝试的那样更新主键:
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
如果你需要处理删除的项目,最好检测它们并存储在一个单独的列表中,然后为每个项目手动调用存储库删除方法,其中存储库删除方法将包含一些最终的关于相关实体的附加逻辑。
如果您需要更改相关实体的主键,您需要先从关系中删除该相关实体,然后添加一个具有更新键的新实体。
所以这是更新后的控制器操作,省略了 null 和安全检查:
public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update the master entity properties manually
entity.SomeProperty = "Updated value";
// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();
foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}
// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}
// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}
// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}
// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}
// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);
// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}
// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}
使用这个通用的sub来标记子状态,简单易用
Notes:
- PromatCon: the entity object
- amList: is the child list that you want to add or modify
- rList: is the child list that you want to remove
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(c => c.rid == objCas.rid & !objCas.ECC_Decision.Select(x => x.dcid).Contains(c.dcid)).toList())
public void updatechild<Ety>(ICollection<Ety> amList, ICollection<Ety> rList)
{
foreach (var obj in amList)
{
var x = PromatCon.Entry(obj).GetDatabaseValues();
if (x == null)
PromatCon.Entry(obj).State = EntityState.Added;
else
PromatCon.Entry(obj).State = EntityState.Modified;
}
foreach (var obj in rList.ToList())
PromatCon.Entry(obj).State = EntityState.Deleted;
}
PromatCon.SaveChanges()
我正在为一些看似基本的操作而苦苦挣扎。
假设我有一个 class 名为 Master:
public class Master
{
public Master()
{
Children = new List<Child>();
}
public int Id { get; set; }
public string SomeProperty { get; set; }
[ForeignKey("SuperMasterId")]
public SuperMaster SuperMaster { get; set; }
public int SuperMasterId { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string SomeDescription { get; set; }
public decimal Count{ get; set; }
[ForeignKey("RelatedEntityId")]
public RelatedEntity RelatedEntity { get; set; }
public int RelatedEntityId { get; set; }
[ForeignKey("MasterId")]
public Master Master { get; set; }
public int MasterId { get; set; }
}
我们有一个这样的控制器动作:
public async Task<OutputDto> Update(UpdateDto updateInput)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update properties
entity.SomeProperty = "Updated value";
entity.SuperMaster.Id = updateInput.SuperMaster.Id;
foreach (var child in input.Children)
{
if (entity.Children.All(x => x.Id != child.Id))
{
// This input child doesn't exist in entity.Children -- add it
// Mapper.Map uses AutoMapper to map from the input DTO to entity
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// The input child exists in entity.Children -- update it
var oldChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (oldChild == null)
{
continue;
}
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
}
foreach (var child in entity.Children.Where(x => x.Id != 0).ToList())
{
if (input.Children.All(x => x.Id != child.Id))
{
// The child doesn't exist in input anymore, mark it for deletion
child.Id = -1;
}
}
entity = await _masterRepository.UpdateAsync(entity);
// Use AutoMapper to map from entity to DTO
return MapToEntityDto(entity);
}
现在的存储库方法(MasterRepository):
public async Task<Master> UpdateAsync(Master entity)
{
var superMasterId = entity.SuperMaster.Id;
// Make sure SuperMaster properties are updated in case the superMasterId is changed
entity.SuperMaster = await Context.SuperMasters
.FirstOrDefaultAsync(x => x.Id == superMasterId);
// New and updated children, skip deleted
foreach (var child in entity.Children.Where(x => x.Id != -1))
{
await _childRepo.InsertOrUpdateAsync(child);
}
// Handle deleted children
foreach (var child in entity.Children.Where(x => x.Id == -1))
{
await _childRepo.DeleteAsync(child);
entity.Children.Remove(child);
}
return entity;
}
最后,来自ChildrenRepository的相关代码:
public async Task<Child> InsertOrUpdateAsync(Child entity)
{
if (entity.Id == 0)
{
return await InsertAsync(entity, parent);
}
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
// We have already updated child properties in the controller method
// and it's expected that changed entities are marked as changed in EF change tracker
return entity;
}
public async Task<Child> InsertAsync(Child entity)
{
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
entity = Context.Set<Child>().Add(entity).Entity;
// We need the entity Id, hence the call to SaveChanges
await Context.SaveChangesAsync();
return entity;
}
Context
属性 实际上是 DbContext
并且事务是在操作过滤器中启动的。如果操作抛出异常,则操作过滤器执行回滚,如果没有,则调用 SaveChanges。
发送的输入对象如下所示:
{
"someProperty": "Some property",
"superMaster": {
"name": "SuperMaster name",
"id": 1
},
"children": [
{
"relatedEntity": {
"name": "RelatedEntity name",
"someOtherProp": 20,
"id": 1
},
"count": 20,
"someDescription": "Something"
}],
"id": 10
}
Masters
table 当前有一个 ID 为 10 的记录。它没有子项。
抛出的异常是:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
这是怎么回事?我认为 EF 应该跟踪更改,包括知道我们在该内部方法中调用了 SaveChanges。
EDIT 删除对 SaveChanges 的调用没有任何改变。此外,在观察 SQL Server Profiler 中发生的情况时,我找不到任何由 EF 生成的 INSERT 或 UPDATE SQL 语句。
EDIT2调用SaveChanges时有INSERT语句,但仍然没有Master实体的UPDATE语句
像往常一样,将这个问题发布到 Whosebug 帮助我解决了问题。代码本来不像上面的问题,但是我在写问题的同时修复了代码。
在写这个问题之前,我花了将近一天的时间试图找出问题所在,所以我尝试了不同的东西,比如重新创建实体实例并手动附加它们,将一些实体标记为 Unchanged/Modified,使用 AsNoTracking 甚至完全禁用所有实体的自动更改跟踪并手动标记所有实体添加或修改。
原来导致这个问题的代码是在 child 存储库的私有方法中,我认为它不相关,所以我省略了它。不过,如果我没有忘记从中删除一些手动更改跟踪代码,它确实不相关,这些代码基本上会干扰 EF 的自动更改跟踪器并导致其行为异常。
但是,感谢 Whosebug,问题得到了解决。当你和别人谈论这个问题时,你需要自己 re-analyze 来解释它的所有细节,以便与你交谈的人(在这种情况下,SO 社区)能够理解它。当你 re-analyze 它时,你会注意到所有小 problem-causing 位,然后更容易诊断问题。
所以无论如何,如果有人因为标题而被这个问题吸引,通过 google 搜索或 w/e,这里有一些要点:
如果您要更新多个级别的实体,请始终调用
.Include
以在获取现有实体时包含所有相关的导航属性。这将使它们全部加载到更改跟踪器中,您将不需要手动 attach/mark。完成更新后,调用 SaveChanges 将正确保存所有更改。当您需要更新 child 实体时,不要对 top-level 实体使用 AutoMapper,尤其是当您必须在更新 child 时实现一些额外的逻辑时]ren.
永远不要像我在将 Id 设置为 -1 时尝试的那样更新主键,或者像我在控制器更新方法的这一行中尝试的那样更新主键:
// The mapper will also update child.RelatedEntity.Id Mapper.Map(child, oldChild);
如果你需要处理删除的项目,最好检测它们并存储在一个单独的列表中,然后为每个项目手动调用存储库删除方法,其中存储库删除方法将包含一些最终的关于相关实体的附加逻辑。
如果您需要更改相关实体的主键,您需要先从关系中删除该相关实体,然后添加一个具有更新键的新实体。
所以这是更新后的控制器操作,省略了 null 和安全检查:
public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update the master entity properties manually
entity.SomeProperty = "Updated value";
// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();
foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}
// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}
// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}
// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}
// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}
// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);
// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}
// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}
使用这个通用的sub来标记子状态,简单易用
Notes:
- PromatCon: the entity object
- amList: is the child list that you want to add or modify
- rList: is the child list that you want to remove
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(c => c.rid == objCas.rid & !objCas.ECC_Decision.Select(x => x.dcid).Contains(c.dcid)).toList())
public void updatechild<Ety>(ICollection<Ety> amList, ICollection<Ety> rList)
{
foreach (var obj in amList)
{
var x = PromatCon.Entry(obj).GetDatabaseValues();
if (x == null)
PromatCon.Entry(obj).State = EntityState.Added;
else
PromatCon.Entry(obj).State = EntityState.Modified;
}
foreach (var obj in rList.ToList())
PromatCon.Entry(obj).State = EntityState.Deleted;
}
PromatCon.SaveChanges()