c# - EF Core - 在一个请求中添加/更新实体和添加/更新/删除子实体

标签 c# entity-framework asp.net-core entity-framework-core aspnetboilerplate

我正在努力处理一些看似基本的操作。

假设我有一个名为 Master 的类(class):

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; }
}

我们有一个这样的 Controller Action :

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 表当前有一个 ID 为 10 的记录。它没有子项。

抛出的异常是:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException:数据库操作预计会影响 1 行,但实际上影响了 0 行。加载实体后数据可能已被修改或删除。

这是怎么回事?我认为 EF 应该跟踪更改,包括知道我们在该内部方法中调用了 SaveChanges。

编辑 删除对 SaveChanges 的调用没有任何改变。此外,在观察 SQL Server Profiler 中发生的情况时,我找不到任何由 EF 生成的 INSERT 或 UPDATE SQL 语句。

EDIT2 调用 SaveChanges 时有 INSERT 语句,但主实体仍然没有 UPDATE 语句。

最佳答案

像往常一样,将这个问题发布到 StackOverflow 帮助我解决了问题。代码本来不像上面的问题,但是我在写问题的同时修复了代码。

在写这个问题之前,我花了将近一天的时间试图找出问题所在,所以我尝试了不同的方法,例如重新创建实体实例并手动附加它们,将一些实体标记为未更改/已修改,使用 AsNoTracking 甚至完全禁用所有实体的自动更改跟踪,并手动标记所有实体的添加或修改。

原来导致这个问题的代码是在那个子存储库的私有(private)方法中,我忽略了它,因为我认为它不相关。不过,如果我没有忘记从中删除一些手动更改跟踪代码,它确实不相关,这些代码基本上会干扰 EF 的自动更改跟踪器并导致其行为异常。

但是,感谢 StackOverflow,问题得到了解决。当您与某人谈论问题时,您需要自己重新分析它,以便能够解释它的所有细节,以便与您交谈的人(在本例中为 SO 社区)理解它。当您重新分析它时,您会注意到所有导致问题的小细节,然后更容易诊断问题。

所以无论如何,如果有人因为标题而被这个问题吸引,通过谷歌搜索或 w/e,这里有一些要点:

  • 如果您要更新多个级别的实体,请始终调用 .Include 以在获取现有实体时包含所有相关的导航属性。这将使它们全部加载到更改跟踪器中,您不需要手动附加/标记。完成更新后,调用 SaveChanges 将正确保存所有更改。

  • 当您需要更新子实体时,不要对顶级实体使用 AutoMapper,尤其是当您必须在更新子实体时实现一些额外的逻辑时。

  • 永远不要像我在将 Id 设置为 -1 时尝试的那样更新主键,或者像我在 Controller 更新方法的这一行中尝试的那样更新主键:

    //映射器也会更新 child.RelatedEntity.Id Mapper.Map(child, oldChild);

  • 如果您需要处理已删除的项目,最好检测它们并将它们存储在单独的列表中,然后为它们中的每一个手动调用存储库删除方法,其中存储库删除方法将包含一些最终的附加逻辑相关实体。

  • 如果您需要更改相关实体的主键,您需要先从关系中删除该相关实体,然后添加一个具有更新键的新实体。

所以这里是更新后的 Controller 操作,省略了 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);
}

关于c# - EF Core - 在一个请求中添加/更新实体和添加/更新/删除子实体,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48359363/

相关文章:

c# - 使用求和函数得到错误答案

c# - ExecuteNonQuery 离开休眠等待命令 session

c# - 为什么在显式 getter-only 接口(interface)实现上使用私有(private) setter 是非法的?

entity-framework - 使用 Entity Framework 6 从联结表中删除但将记录保留在查找表中

c# - 模型状态错误 : The value 'null' is not valid for nullable field

c# - Excel 2007 : XML: choking on missing ss: namespace qualifier

c# - MVC 嵌套模型集合

c# - Linq 查询未按预期运行

asp.net-mvc - 基于验证禁用表单提交

c# - 如何从 ASP.Net Core 配置文件中合并多个数组?