虽然查询允许从数据库读取数据,但保存数据意味着向数据库添加新实体、删除实体或修改现有实体的属性。 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();
}
上述代码执行以下步骤:
- 它使用常规 LINQ 查询从数据库加载实体(请参阅 查询数据)。 EF 的查询默认是跟踪的,这意味着 EF 会在其内部 更改跟踪器中跟踪加载的实体。
- 可以像往常一样操作加载的实体实例,通过分配一个 .NET 属性。 此步骤不涉及 EF。
- 最后, DbContext.SaveChanges() 被调用。 此时,EF 通过将实体与加载后的快照进行比较来自动检测任何更改。 检测到的任何更改都保存到数据库;使用关系数据库时,这通常涉及发送 SQL
UPDATE
来更新相关行。
请注意,上面描述了现有数据的典型更新操作,但添加和删除实体时也存在相似的原则。 通过调用 DbSet<TEntity>.Add 和 Remove 来与 EF 的更改跟踪器交互,从而实现跟踪更改。 EF 然后在调用 SaveChanges() 时(例如,通过 SQL INSERT
和 DELETE
使用关系数据库)将所有跟踪的更改应用到数据库。
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()。 对于此类情境,ExecuteUpdate
和 ExecuteDelete
可以通过一种更简单的方式来表达相同的操作。
最后,更改跟踪和 SaveChanges() 本身都会产生特定的运行时开销。 如果您正在编写高性能应用程序,ExecuteUpdate
和 ExecuteDelete
可以帮助您避开这两个组件,并高效地生成所需的语句。
但是,请注意 ExecuteUpdate
,并且 ExecuteDelete
也有一些限制:
- 这些方法立即执行,当前无法与其他操作一起批处理。 另一方面,SaveChanges()可以将多个操作一起批处理。
- 由于不涉及更改跟踪,因此确切地知道需要更改哪些实体和属性是你的责任。 这可能意味着更手动、低级别代码跟踪需要更改的内容以及哪些内容不更改。
- 此外,由于不涉及更改跟踪,因此在保留更改时,这些方法不会自动应用 并发控制 。 但是,你仍然可以显式添加一个
Where
子句来实现并发控制。 - 目前仅支持更新和删除;必须通过 DbSet<TEntity>.Add 和 SaveChanges() 完成插入。
有关详细信息和代码示例,请参阅 ExecuteUpdate
和 ExecuteDelete
。
概要
下面是一些关于何时使用哪种方法的准则。 请注意,这些规则不是绝对规则,而是提供有用的经验规则:
- 如果事先不知道将进行哪些更改,请使用
SaveChanges
;它将自动检测需要应用哪些更改。 示例方案:- “我想从数据库加载博客,并显示一个表单,允许用户更改它”
- 如果需要作对象图(即多个互连对象),请使用
SaveChanges
;它将找出更改的正确顺序以及如何将所有内容链接在一起。- “我想更新博客,更改其一些帖子并删除其他文章”
- 如果希望基于某个条件更改大量实体,请使用
ExecuteUpdate
和ExecuteDelete
。 示例方案:- “我想给所有员工加薪”
- “我想删除名称以 X 开头的所有博客”
- 如果已确切知道要修改的实体以及更改它们的方式,请使用
ExecuteUpdate
和ExecuteDelete
。 示例方案:- “我想删除名为”Foo“的博客
- “我想将 ID 为 5 的博客名称更改为”Bar“”