保存数据
虽然查询允许从数据库中读取数据,但保存数据意味着向数据库添加新实体、删除实体或以某种方式修改现有实体的属性。 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();
}
上述代码执行以下步骤:
- 它使用常规 LINQ 查询从数据库加载实体(请参阅查询数据)。 默认情况下跟踪 EF 的查询,这意味着 EF 在其内部更改跟踪器中跟踪加载的实体。
- 通过分配 .NET 属性来照常操作加载的实体实例。 此步骤不涉及 EF。
- 最后调用 DbContext.SaveChanges()。 此时,EF 会自动检测任何更改,方法是将实体与加载实体时的快照进行比较。 检测到的任何更改都将保存到数据库;使用关系数据库时,这通常涉及发送 SQL
UPDATE
来更新相关行。
请注意,上面描述了现有数据的典型更新操作,但添加和删除实体时遵循类似的原则。 通过调用 DbSet<TEntity>.Add 和 Remove 与 EF 的更改跟踪器交互,从而跟踪更改。 然后,当调用 SaveChanges() 时,EF 会将所有跟踪的更改应用于数据库(例如,在使用关系数据库时通过 SQL INSERT
和 DELETE
)。
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()。 对于此类方案,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'”