保存数据

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

方法 1:更改跟踪和 SaveChanges

在许多情况下,程序需要查询数据库中的某些数据,对它执行一些修改,然后将这些修改保存回来;这有时称为“工作单位”。 例如,假设你有一组博客,并且你想要更改 Url 其中一个博客的属性。 在 EF 中,这种操作通常如下进行:

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

上述代码执行以下步骤:

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

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

SaveChanges() 提供以下优势:

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

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

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

虽然更改跟踪和 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 可以帮助您避开这两个组件,并高效地生成所需的语句。

但是,请注意 ExecuteUpdate ,并且 ExecuteDelete 也有一些限制:

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

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

概要

下面是一些关于何时使用哪种方法的准则。 请注意,这些规则不是绝对规则,而是提供有用的经验规则:

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