保存数据

虽然查询允许从数据库中读取数据,但保存数据意味着向数据库添加新实体、删除实体或以某种方式修改现有实体的属性。 Entity Framework Core (EF Core) 支持将数据保存到数据库的两种基本方法。

方法 1:更改跟踪和 SaveChanges

在许多情况下,程序需要查询数据库中的某些数据,对其执行一些修改,并保存这些修改;这有时称为“工作单元”。 例如,假设你有一组博客,并且你想要更改其中一个博客的 Url 属性。 在 EF 中,这通常按如下方式完成:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Single(b => b.Url == "http://example.com");
    blog.Url = "http://example.com/blog";
    context.SaveChanges();
}

上述代码执行以下步骤:

  1. 它使用常规 LINQ 查询从数据库加载实体(请参阅查询数据)。 默认情况下跟踪 EF 的查询,这意味着 EF 在其内部更改跟踪器中跟踪加载的实体。
  2. 通过分配 .NET 属性来照常操作加载的实体实例。 此步骤不涉及 EF。
  3. 最后调用 DbContext.SaveChanges()。 此时,EF 会自动检测任何更改,方法是将实体与加载实体时的快照进行比较。 检测到的任何更改都将保存到数据库;使用关系数据库时,这通常涉及发送 SQL UPDATE 来更新相关行。

请注意,上面描述了现有数据的典型更新操作,但添加和删除实体时遵循类似的原则。 通过调用 DbSet<TEntity>.AddRemove 与 EF 的更改跟踪器交互,从而跟踪更改。 然后,当调用 SaveChanges() 时,EF 会将所有跟踪的更改应用于数据库(例如,在使用关系数据库时通过 SQL INSERTDELETE)。

SaveChanges() 提供以下优势:

  • 无需编写代码来跟踪已更改的实体和属性 - EF 会自动为你执行此操作,并且仅更新数据库中的这些属性,从而提高性能。 想象一下,如果加载的实体绑定到 UI 组件,允许用户更改他们想要的任何属性;EF 减轻了找出哪些实体和属性实际已更改的负担。
  • 保存对数据库的更改有时可能很复杂! 例如,如果要添加一个博客并为该博客添加一些帖子,则可能需要为插入的博客提取数据库生成的密钥,然后才能插入帖子(因为它们需要引用博客)。 EF 为你完成所有这些操作,从而消除了复杂性。
  • EF 可以检测并发问题,例如,当其他人在你的查询和 SaveChanges() 之间修改了数据库行时。 并发冲突中提供了更多详细信息。
  • 在支持它的数据库中,SaveChanges() 自动包装事务中的多个更改,确保在发生故障时数据保持一致。 事务中提供了更多详细信息。
  • 在许多情况下,SaveChanges() 还会对多个更改进行批处理,从而显著减少数据库往返次数并大幅提高性能。 高效更新中提供了更多详细信息。

有关基本 SaveChanges() 用法的详细信息和代码示例,请参阅基本 SaveChanges。 有关 EF 更改跟踪的详细信息,请参阅更改跟踪概述

方法 2:ExecuteUpdate 和 ExecuteDelete(“批量更新”)

注意

EF Core 7.0 中已引入此功能。

虽然更改跟踪和 SaveChanges() 是保存更改的强大方法,但它们确实存在某些缺点。

首先,SaveChanges() 要求查询和跟踪要修改或删除的所有实体。 如果需要删除评分低于特定阈值的所有博客,则必须查询、具体化和跟踪可能大量的行,并让 SaveChanges() 为每个行生成 DELETE 语句。 关系数据库提供了一个更高效的替代方法:可以发送单个 DELETE 命令,通过 WHERE 子句指定要删除的行,但 SaveChanges() 模型不允许生成该行。

若要支持此“批量更新”方案,可以使用 ExecuteDelete,如下所示:

context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();

这允许通过常规 LINQ 运算符(类似于常规 LINQ 查询)来表达 SQL DELETE 语句,从而针对数据库执行以下 SQL:

DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

这会在数据库中非常高效地执行,无需从数据库加载任何数据或涉及 EF 的更改跟踪器。 同样,ExecuteUpdate 允许表达 SQL UPDATE 语句。

即使未批量更改实体,也可能确切地知道要更改哪个实体的哪些属性。 使用更改跟踪 API 执行更改可能过于复杂,需要创建实体实例,通过 Attach 跟踪它,进行更改,最后调用 SaveChanges()。 对于此类方案,ExecuteUpdateExecuteDelete 可能是表示相同操作的更简单方法。

最后,更改跟踪和 SaveChanges() 本身都会产生特定的运行时开销。 如果要编写高性能应用程序,则使用 ExecuteUpdateExecuteDelete 可以避免这两个组件并有效地生成所需的语句。

但是,请注意,ExecuteUpdateExecuteDelete 也有一些限制:

  • 这些方法会立即执行,目前无法与其他操作一起批处理。 另一方面,SaveChanges() 可以同时批处理多个操作。
  • 由于不涉及更改跟踪,因此你有责任确切地知道需要更改哪些实体和属性。 这可能意味着更多的手动、低级别代码跟踪需要更改的内容和不需要更改的内容。
  • 此外,由于不涉及更改跟踪,因此在保留更改时,这些方法不会自动应用并发控制。 但是,你仍然可以显式添加 Where 子句来自行实现并发控制。
  • 目前仅支持更新和删除;必须通过 DbSet<TEntity>.AddSaveChanges() 完成插入。

有关详细信息和代码示例,请参阅 ExecuteUpdateExecuteDelete

总结

下面是有关何时使用哪种方法的一些准则。 请注意,这些不是绝对规则,但提供了有用规则的缩略图:

  • 如果事先不知道将发生哪些更改,请使用 SaveChanges;它将自动检测需要应用的更改。 示例方案:
    • “我想要从数据库加载博客并显示允许用户更改它的表单”
  • 如果需要操作一个对象图(即多个相互连接的对象),请使用 SaveChanges;它将确定更改的正确顺序以及如何将所有内容链接在一起。
    • “我想要更新博客,更改其中某些帖子并删除其他帖子”
  • 如果要根据某些条件更改可能大量的实体,请使用 ExecuteUpdateExecuteDelete。 示例方案:
    • “我想要给所有员工加薪”
    • “我想要删除名称以 X 开头的所有博客”
  • 如果已经确切地知道要修改哪些实体以及如何更改它们,请使用 ExecuteUpdateExecuteDelete。 示例方案:
    • “我想要删除名称为‘Foo’的博客”
    • “我想要将 ID 为 5 的博客名称更改为 'Bar'”