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

2024-03-04

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

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

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

The 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。

EDIT删除对 SaveChanges 的调用不会改变任何内容。另外,在观察 SQL Server Profiler 中发生的情况时,我找不到 EF 生成的任何 INSERT 或 UPDATE SQL 语句。

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


像往常一样,将这个问题发布到 StackOverflow 帮助我解决了问题。该代码最初看起来不像上面的问题,但我在编写问题时宁愿修复代码。

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

事实证明,导致此问题的代码位于该子存储库的私有方法中,我省略了该方法,因为我认为它不相关。不过,如果我没有忘记从中删除一些手动更改跟踪代码,那么它确实无关紧要,这些代码基本上会干扰 EF 的自动更改跟踪器并导致其行为异常。

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

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

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

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

  • 永远不要像我在将 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);
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

EF Core - 在一个请求中添加/更新实体以及添加/更新/删除子实体 的相关文章

  • 格式说明符%02x

    我有一个简单的程序 include
  • 使用 ADAL v3 使用 ClientID 对 Dynamics 365 进行身份验证

    我正在尝试对我们的在线 Dynamics CRM 进行身份验证以使用可用的 API 我能找到的唯一关于执行此操作的官方文档是 https learn microsoft com en us dynamics365 customer enga
  • 无法继承形状

    为什么我不能使用继承 a 的类Shapes class http msdn microsoft com en us library ms604615 28v vs 90 29 我需要延长Rectangle具有一些方法的类 但我想以与使用相同
  • strlen() 编译时优化

    前几天我发现你可以找到编译时strlen使用这样的东西 template
  • 混合模型优先和代码优先

    我们使用模型优先方法创建了一个 Web 应用程序 一名新开发人员进入该项目 并使用代码优先方法 使用数据库文件 创建了一个新的自定义模型 这 这是代码第一个数据库上下文 namespace WVITDB DAL public class D
  • 如何向 Mono.ZeroConf 注册服务?

    我正在尝试测试 ZeroConf 示例http www mono project com Mono Zeroconf http www mono project com Mono Zeroconf 我正在运行 OpenSuse 11 和 M
  • Android NDK 代码中的 SIGILL

    我在市场上有一个 NDK 应用程序 并获得了有关以下内容的本机崩溃报告 SIGILL信号 我使用 Google Breakpad 生成本机崩溃报告 以下是详细信息 我的应用程序是为armeabi v7a with霓虹灯支持 它在 NVIDI
  • 包恢复失败。回滚包更改 - Serilog.AspNetCore

    我有一个 asp net Core 项目 我正在尝试向其中添加一个记录器 我选择了我在其他项目中使用过的 SeriLog 但是当我尝试添加 Serilog AspNetCore 我得到的软件包版本 2 0 0 包恢复失败 回滚 后端 的包更
  • 如何在 Javascript 中连接 C# ActiveX 事件处理程序

    我尝试使用几个代码片段将 ActiveX 对象与 Javascript 事件处理程序挂钩 我无法确定为什么事件处理程序没有被调用 带有项目的 Github 存储库 https github com JesseKPhillips Csharp
  • 如何从 Boost.PropertyTree 复制子树

    我有一些boost property tree ptree 我需要树来删除一些具有特定标签名称的元素 例如 xml 表示源ptree如下
  • SQLAPI++ 的免费替代品? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 是否有任何免费 也许是开源 的替代品SQLAPI http www sqlapi com 这个库看起来
  • Unity3D - 将 UI 对象移动到屏幕中心,同时保持其父子关系

    我有一个 UI 图像 它的父级是 RectTransform 容器 该容器的父级是 UI 面板 而 UI 面板的父级是 Canvas 我希望能够将此 UI 图像移动到屏幕中心 即画布 同时保留父级层次结构 我的目标是将 UI 图像从中心动画
  • 在哪里可以找到 Microsoft.Build.Utilities.v3.5

    如何获取 Microsoft Build Utilities v3 5 我正在使用 StyleCop 4 7 Stylecop dll 中的 StyleCop msbuild 任务似乎依赖于 Microsoft Build Utilitie
  • C:设置变量范围内所有位的最有效方法

    让我们来int举个例子 int SetBitWithinRange const unsigned from const unsigned to To be implemented SetBitWithinRange应该返回一个int其中所有
  • 如何从 Windows Phone 7 模拟器获取数据

    我有一个 WP7 的单元测试框架 它在手机上运行 结果相当难以阅读 因此我将它们写入 XDocument 我的问题是 如何才能将这个 XML 文件从手机上移到我的桌面上 以便我可以实际分析结果 到目前为止 我所做的是将 Debugger B
  • winform c# 中的弹出窗口

    我正在开发一个需要弹出窗口的项目 但问题是我还希望能够通过表单设计器在此弹出窗口中添加文本框等 所以基本上我有一个按钮 当您单击它时 它将打开我在表单设计器中设计的另一个窗口 我一直在谷歌搜索 但还没有找到我需要的东西 所以我希望你们能帮助
  • 如果找不到指定的图像文件,显示默认图像的最佳方式?

    我有一个普通的电子商务应用程序 我将 ITEM IMAGE NAME 存储在数据库中 有时经理会拼错图像名称 为了避免 丢失图像 IE 中的红色 X 每次显示产品列表时 我都会检查服务器中是否有与该产品相关的图像 如果该文件不存在 我会将其
  • 如何在 C# 中获取 CMD/控制台编码

    我需要指定正确的代码页来使用 zip 库打包文件 正如我所见 我需要指定控制台编码 在我的例子中为 866 C Users User gt mode Status for device CON Lines 300 Columns 130 K
  • 如何在 ASP.NET Core 中注入泛型的依赖关系

    我有以下存储库类 public class TestRepository Repository
  • 嵌入式linux编写AT命令

    我在向 GSM 模块写入 AT 命令时遇到问题 当我使用 minicom b 115200 D dev ttySP0 term vt100 时它工作完美 但我不知道如何在 C 代码中做同样的事情 我没有收到任何错误 但模块对命令没有反应 有

随机推荐