代码之家  ›  专栏  ›  技术社区  ›  Dejan JanjuÅ¡ević Samuel Caillerie

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

  •  5
  • Dejan JanjuÅ¡ević Samuel Caillerie  · 技术社区  · 7 年前

    我正在努力完成一些基本的操作。

    假设我有一个名为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);
    }
    

    现在是repository方法(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: 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。

    编辑 删除SaveChanges调用不会改变任何内容。我也找不到 插入或 在监视SQL Server事件探查器中发生的情况时,更新EF生成的SQL语句。

    编辑2 调用SaveChanges时,INSERT语句就在那里,但主实体仍然没有UPDATE语句。

    2 回复  |  直到 7 年前
        1
  •  16
  •   Dejan JanjuÅ¡ević Samuel Caillerie    7 年前

    像往常一样,将此问题发布到StackOverflow帮助我解决了问题。代码最初与上述问题中的代码不同,但我在编写问题时正在修复代码。

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

    结果表明,导致此问题的代码位于该子存储库的一个私有方法中,我忽略了该方法,因为我认为它与此无关。如果我没有忘记从中删除一些手动更改跟踪代码的话,这真的没有什么关系,因为这些代码基本上是在摆弄EF的自动更改跟踪程序,导致其行为不端。

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

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

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

    • 当需要更新子实体时,不要对顶级实体使用AutoMapper,尤其是在更新子实体时必须实现一些附加逻辑的情况下。

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

      // The mapper will also update child.RelatedEntity.Id Mapper.Map(child, oldChild);

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

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

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

    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);
    }
    
        2
  •  1
  •   Basil    5 年前

    使用此通用sub标记子状态,易于使用

    笔记:

    • PromatCon:实体对象
    • amList:是要添加或修改的子列表
    • rList:是要删除的子列表
    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()