在检查实体是否存在于 EF Core 中时适当更新实体

Appropriate update of entity while checking if it exists in EF Core

我有以下更新实体的方法。我遇到的唯一问题是,当提供一个不存在的 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. 删除现有的并添加新的。

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

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

异常的原因是因为通过从上下文加载实体来检查它是否存在,您现在有一个跟踪引用。当你去更新分离引用时,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。这避免了关于什么对象代表 view/consumer 状态与数据状态的潜在混淆。通过将值从 DTO 显式复制到实体,或配置映射器以复制支持的值,您还可以保护您的系统免受意外篡改和潜在的陈旧覆盖。使用此方法的一个考虑因素是,您应该通过确保您的实体和 DTO 具有 RowVersion/Timestamp 进行比较来保护更新,以避免无条件地用可能过时的数据覆盖数据。在从 DTO 复制到新加载的实体之前,比较版本,如果匹配,则数据行中没有任何更改,因为您获取并组成了 DTO。如果它已更改,则意味着自读取 DTO 以来其他人已经更新了基础数据行,因此您的修改是针对陈旧数据的。从那里,采取适当的行动,例如放弃更改、覆盖更改、合并更改、记录事实等。