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

查看:102
本文介绍了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语句存在,但主实体仍然没有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.

在写问题之前,我花了将近一天的时间来弄清问题所在,因此我尝试了不同的方法,例如重新创建实体实例并手动附加它们,将某些实体标记为Unchanged/Modified,使用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:

//映射器还将更新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.

这是更新的控制器操作,其中包含null并省略了安全检查:

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天全站免登陆