【问题标题】:Appropriate update of entity while checking if it exists in EF Core在检查实体是否存在于 EF Core 时适当更新实体
【发布时间】:2021-12-27 07:16:32
【问题描述】:

我有以下方法更新实体。我唯一的区别是,当提供一个不存在的 ID 时,我得到了一个严重的例外。

public bool Update(Thing thing)
{
    Context.Things.Update(thing);
    int result = Context.SaveChanges();

    return result == 1;
}

所以我添加了一个检查来控制抛出的异常(加上一些不错的日志记录和其他便利)。最终,我打算完全跳过呕吐。

public bool UpdateWithCheck(Thing thing)
{
    Thing target = Context.Things.SingleOrDefault(a => a.Id == thing.Id);
    if (target == null)
        throw new CustomException($"No thing with ID {thing.Id}.");

    Context.Things.Update(thing);
    int result = Context.SaveChanges();

    return result == 1;
}

不,这不起作用,因为实体已经被跟踪。我有几个选择来处理这个问题。

  1. 更改为Context.Where(...).AsNoTracking()
  2. 在目标中显式设置更新的字段并保存。
  3. 到处乱搞实体状态并篡改跟踪器。
  4. 删除当前并添加新的。

我无法决定哪个是最佳做法。谷歌搜索给了我在同一操作中不包含检查预先存在状态的默认示例。

【问题讨论】:

    标签: c# entity-framework asp.net-core-5.0


    【解决方案1】:

    异常的原因是因为通过从上下文加载实体来检查它是否存在,你现在有了一个跟踪的引用。当你去更新分离的引用时,EF 会抱怨一个实例已经被跟踪。

    最简单的解决方法是:

    public bool UpdateWithCheck(Thing thing)
    {
        bool doesExist = Context.Things.Any(a => a.Id == thing.Id);
        if (!doesExist)
            throw new CustomException($"No thing with ID {thing.Id}.");
    
        Context.Things.Update(thing);
        int result = Context.SaveChanges();
    
        return result == 1;
    }
    

    但是,这种方法存在两个问题。首先,因为我们不知道 DbContext 实例的范围或无法保证方法的顺序,所以在某些时候 DbContext 实例可能已经加载并跟踪了该事物的实例。这可能表现为看似间歇性的错误。防止这种情况的正确方法是:

    public bool UpdateWithCheck(Thing thing)
    {
        bool doesExist = Context.Things.Any(a => a.Id == thing.Id);
        if (!doesExist)
            throw new CustomException($"No thing with ID {thing.Id}.");
    
        Thing existing = Context.Things.Local.SingleOrDefault(a => a.Id == thing.Id);
        if (existing != null)
            Context.Entry(existing).State = EntityState.Detached;
    
        Context.Things.Update(thing);
        int result = Context.SaveChanges();
    
        return result == 1;
    }
    

    这会检查任何已加载实例的本地跟踪缓存,如果找到,则将它们分离。这里的风险是,任何未保留在这些跟踪引用中的修改都将被丢弃,并且任何漂浮在周围的假设已附加,现在将被分离。

    第二个重要问题是使用Update()。当您传递分离的实体时,您不打算更新的数据可能会被更新。更新将替换所有列,通常情况下,如果客户端可能只需要更新其中的一个子集。当您的数据库设置为支持它们(例如快照隔离)时,可以将 EF 配置为在更新之前根据数据库检查实体上的行版本或时间戳,这有助于防止过时的覆盖,但仍允许意外篡改。

    正如您已经发现的那样,更好的方法是避免传递分离的实体,而是使用专用的 DTO。这避免了关于哪些对象代表视图/消费者状态与数据状态的潜在混淆。通过将值从 DTO 显式复制到实体,或配置映射器以复制支持的值,您还可以保护您的系统免受意外篡改和潜在的过时覆盖。这种方法的一个考虑因素是,您应该通过确保您的实体和 DTO 具有要比较的 RowVersion/Timestamp 来保护更新以避免无条件地用可能过时的数据覆盖数据。在从 DTO 复制到新加载的实体之前,请比较版本,如果它匹配,则自从您获取并编写 DTO 以来,数据行中没有任何变化。如果它已更改,则意味着其他人在读取 DTO 后更新了基础数据行,因此您的修改是针对陈旧数据的。从那里,采取适当的措施,例如放弃更改、覆盖更改、合并更改、记录事实等。

    【讨论】:

      【解决方案2】:

      只需更改target 的属性并调用SaveChanges() - 删除更新调用。我想说这些天的典型用例是输入thing 实际上不是Thing,而是ThingViewModelThingDto 或其他主题的变体“承载足够多的对象识别和更新事物但实际上不是数据库实体的数据”。就此而言,如果手动从 ThingViewModel 更新 Thing 属性的想法让您感到厌烦,您可以查看映射器(AutoMapper 可能是最知名的,但还有很多其他的)来为您进行复制,甚至设置您如果您决定将此方法转换为 Upsert,则可以使用新事物

      【讨论】:

      • 呵呵,遥遥领先。在实际代码中,我得到一个ThingDto 并使用 AutoMapper 在控制器中转换它,它会生成一个我发送给服务的域对象。这就像bool result = await Service.Update(dto.ToDomain(...))(使用扩展方法ToDomain(...) 应用AutoMapie)。所以我最终得到了服务中域实体的并发版本。我可以通过 DTO,但在控制器中拥有 DTO 和 VM 以及在服务中拥有域垃圾似乎非常好。有点整洁......
      猜你喜欢
      • 2015-09-25
      • 1970-01-01
      • 1970-01-01
      • 2022-10-18
      • 1970-01-01
      • 1970-01-01
      • 2022-12-09
      • 2021-12-11
      • 1970-01-01
      相关资源
      最近更新 更多