EF Core - 在一个请求中添加/更新实体和添加/更新/删除子实体 [英] EF Core - adding/updating entity and adding/updating/removing child entities in one request

查看:118
本文介绍了EF Core - 在一个请求中添加/更新实体和添加/更新/删除子实体的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为一些看似基本的操作而苦苦挣扎.

I am struggling with what seemed to be a couple of basic operations.

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

Let's say I've got a class named 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; }
}

我们有一个像这样的控制器动作:

We have a controller action like this:

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):

Now the repository method (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 的相关代码:

And finally, the relevant code from 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.

The Context property is actually DbContext and the transaction is started in an action filter. If the action throws an exception, the action filter performs the rollback, and if not, it calls SaveChanges.

发送的输入对象如下所示:

The input object being sent looks like this:

{
  "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.它没有子项.

The Masters table currently has one record with Id 10. It has no children.

抛出的异常是:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException:数据库操作预期影响 1 行,但实际影响 0 行.自加载实体以来,数据可能已被修改或删除.

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

What's going on here? I thought EF was supposed to track changes and that includes knowing that we called SaveChanges in that inner method.

EDIT 删除对 SaveChanges 的调用没有任何改变.此外,在查看 SQL Server Profiler 中发生的情况时,我找不到 EF 生成的任何 INSERT 或 UPDATE SQL 语句.

EDIT Removing that call to SaveChanges changes nothing. Also, I couldn't find any INSERT or UPDATE SQL statement generated by EF when watching what happens in SQL Server Profiler.

EDIT2 调用 SaveChanges 时有 INSERT 语句,但仍然没有 Master 实体的 UPDATE 语句.

EDIT2 The INSERT statement is there when SaveChanges is called, but still there is no UPDATE statement for Master entity.

推荐答案

像往常一样,将此问题发布到 StackOverflow 帮助我解决了问题.代码最初看起来不像上面的问题,但我在编写问题时更喜欢修复代码.

As usual, posting this question to StackOverflow helped me resolve the problem. The code originally didn't look like in the question above, but I was rather fixing the code while writing the question.

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

Before writing the question, I spent almost a day trying to figure out what the problem was and so I tried different things, such as recreating entity instances and attaching them manually, marking some entities as Unchanged/Modified, using AsNoTracking or even completely disabling automatic change tracking for all entities and marking all of them Added or Modified manually.

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

Turned out the code which caused this issue was in a private method of that child repository which I omitted as I didn't think it was relevant. It truly wouldn't be relevant though, if I haven't forgot to remove some manual change tracking code from it, which basically fiddled with EF's automatic change tracker and caused it to misbehave.

但是,感谢 StackOverflow,问题得到了解决.当您与某人谈论问题时,您需要自己重新分析它,以便能够解释它的所有细节,以便与您交谈的人(在这种情况下,SO 社区)理解它.当您重新分析它时,您会注意到所有引起问题的小问题,然后更容易诊断问题.

But, thanks to StackOverflow, the problem was solved. When you talk to someone about the problem, you need to re-analyze it yourself, to be able to explain all the little bits of it in order for that someone who you talk to (in this case, SO community) to understand it. While you re-analyze it, you notice all the little problem-causing bits and then it's easier to diagnose the issue.

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

So anyway, if someone gets attracted to this question because of the title, via google search or w/e, here are some key points:

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

  • If you are updating entities on multiple levels, always call .Include to include all related navigation properties when getting the existing entity. This will make all of them loaded into the change tracker and you won't need to attach/mark manually. After you're done updating, a call to SaveChanges will save all your changes properly.

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

Don't use AutoMapper for the top-level entity when you need to update child entities, especially if you have to implement some additional logic when updating children.

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

Never ever update primary keys like I tried to when setting the Id to -1, or like I tried to on this line right here in the controller Update method:

//mapper 也会更新 child.RelatedEntity.IdMapper.Map(child, oldChild);

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

If you need to handle deleted items, better detect them and store in a separate list, and then manually call the repository delete method for each of them, where the repository delete method would contain some eventual additional logic regarding the related entities.

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

If you need to change the primary key of a related entity, you need to first remove that related entity from the relation and just add a new one with an updated key.

所以这里是更新的控制器操作,省略了空值和安全检查:

So here is the updated controller action with null and safety checks omitted:

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

这篇关于EF Core - 在一个请求中添加/更新实体和添加/更新/删除子实体的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆