访问跟踪的实体
有四个用于访问 DbContext 跟踪的实体的主要 API:
- DbContext.Entry 为指定实体实例返回 EntityEntry<TEntity> 实例。
- ChangeTracker.Entries 为所有跟踪的实体或某种指定类型的所有跟踪的实体返回 EntityEntry<TEntity> 实例。
- DbContext.Find、DbContext.FindAsync、DbSet<TEntity>.Find 和 DbSet<TEntity>.FindAsync 按主键查找单个实体,首先查找跟踪的实体,然后根据需要查询数据库。
- DbSet<TEntity>.Local 为由 DbSet 表示的实体类型的实体返回实际实体(不是 EntityEntry 实例)。
以下部分详细介绍了其中每一个 API。
提示
本文档假设你已了解实体状态和 EF Core 更改跟踪的基础知识。 有关这些主题的详细信息,请参阅 EF Core 中的更改跟踪。
提示
通过从 GitHub 下载示例代码,你可运行并调试到本文档中的所有代码。
使用 DbContext.Entry 和 EntityEntry 实例
对于每个跟踪的实体,Entity Framework Core (EF Core) 跟踪以下内容:
- 实体的总体状态。 状态为
Unchanged
、Modified
、Added
或Deleted
;有关这些详细信息,请参阅 EF Core 中的更改跟踪。 - 跟踪的实体之间的关系。 例如,一篇帖子所属的博客。
- 属性的“当前值”。
- 属性的“原始值”(如果此信息可用)。 原始值是从数据库中查询实体时存在的属性值。
- 自查询后修改的属性值。
- 有关属性值的其他信息,例如,值是否是临时的。
将实体实例传递到 DbContext.Entry 会导致 EntityEntry<TEntity> 为给定实体提供对此信息的访问权限。 例如:
using var context = new BlogsContext();
var blog = context.Blogs.Single(e => e.Id == 1);
var entityEntry = context.Entry(blog);
以下部分显示如何使用 EntityEntry 访问和操作实体状态以及实体的属性和导航状态。
使用实体
EntityEntry<TEntity> 最常见用途是访问实体的当前 EntityState。 例如:
var currentState = context.Entry(blog).State;
if (currentState == EntityState.Unchanged)
{
context.Entry(blog).State = EntityState.Modified;
}
Entry 方法还可用于尚未跟踪的实体。 这不会使得开始跟踪实体;实体的状态仍为 Detached
。 但是,随后可以使用返回的 EntityEntry 更改实体状态,此时将在给定状态下跟踪实体。 例如,以下代码将开始以 Added
状态跟踪 Blog 实例:
var newBlog = new Blog();
Debug.Assert(context.Entry(newBlog).State == EntityState.Detached);
context.Entry(newBlog).State = EntityState.Added;
Debug.Assert(context.Entry(newBlog).State == EntityState.Added);
提示
与在 EF6 中不同,设置单个实体的状态不会导致跟踪所有连接的实体。 这使得以这种方式设置状态成为比调用 Add
、Attach
或 Update
(在整个实体图上进行操作)的级别更低的操作。
下表总结了使用 EntityEntry 处理整个实体的方法:
EntityEntry 成员 | 说明 |
---|---|
EntityEntry.State | 获取并设置实体的 EntityState。 |
EntityEntry.Entity | 获取实体实例。 |
EntityEntry.Context | 正在跟踪此实体的 DbContext。 |
EntityEntry.Metadata | 实体类型的 IEntityType 元数据。 |
EntityEntry.IsKeySet | 实体是否已设置其键值。 |
EntityEntry.Reload() | 使用从数据库中读取的值覆盖属性值。 |
EntityEntry.DetectChanges() | 仅强制检测此实体的更改;请参阅更改检测和通知。 |
使用单个属性
EntityEntry<TEntity>.Property 的多个重载允许访问关于实体的单个属性的信息。 例如,使用强类型的流畅 API:
PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property(e => e.Name);
属性名称可以作为字符串传递。 例如:
PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property<string>("Name");
然后可以将返回的 PropertyEntry<TEntity,TProperty> 用于访问属性相关信息。 例如,它可用于获取和设置该实体上的属性的当前值:
string currentValue = context.Entry(blog).Property(e => e.Name).CurrentValue;
context.Entry(blog).Property(e => e.Name).CurrentValue = "1unicorn2";
以上使用的两种属性方法均返回强类型的泛型 PropertyEntry<TEntity,TProperty> 实例。 使用此泛型类型是首选方法,因为此方法无需装箱值类型即可访问属性值。 但是,如果实体或属性的类型在编译时未知,则可以改为获取非泛型的 PropertyEntry:
PropertyEntry propertyEntry = context.Entry(blog).Property("Name");
这样,无论属性是何种类型,都可以访问任何属性的属性信息,但需支付装箱值类型的费用。 例如:
object blog = context.Blogs.Single(e => e.Id == 1);
object currentValue = context.Entry(blog).Property("Name").CurrentValue;
context.Entry(blog).Property("Name").CurrentValue = "1unicorn2";
下表汇总了由 PropertyEntry 公开的属性信息:
PropertyEntry 成员 | 说明 |
---|---|
PropertyEntry<TEntity,TProperty>.CurrentValue | 获取并设置属性的当前值。 |
PropertyEntry<TEntity,TProperty>.OriginalValue | 获取并设置属性的原始值(如果可用)。 |
PropertyEntry<TEntity,TProperty>.EntityEntry | 对实体的 EntityEntry<TEntity> 的后向引用。 |
PropertyEntry.Metadata | 属性的 IProperty 元数据。 |
PropertyEntry.IsModified | 指示此属性是否被标记为已修改,并允许更改此状态。 |
PropertyEntry.IsTemporary | 指示此属性是否被标记为临时,并允许更改此状态。 |
注意:
- 属性的原始值是从数据库中查询实体时该属性具有的值。 但是,如果实体已断开连接,然后显式附加到另一个 DbContext(例如使用
Attach
或Update
),则原始值不可用。 在这种情况下,返回的原始值将与当前值相同。 - SaveChanges 将仅更新标记为已修改的属性。 将 IsModified 设置为 true 可强制 EF Core 更新给定的属性值,或将其设置为 false 可防止 EF Core 更新属性值。
- 临时值通常由 EF Core 值生成器生成。 设置属性的当前值会将临时值替换为给定值,并将该属性标记为非临时。 将 IsTemporary 设置为 true 可强制将值设置为临时值,即使已对该值进行显式设置。
使用单个导航
EntityEntry<TEntity>.Reference、EntityEntry<TEntity>.Collection 和 EntityEntry.Navigation 的多个重载允许访问关于单个导航的信息。
可通过 Reference 方法访问对单个相关实体的引用导航。 引用导航指向一对多关系的“一”侧,并指向一对一关系的两侧。 例如:
ReferenceEntry<Post, Blog> referenceEntry1 = context.Entry(post).Reference(e => e.Blog);
ReferenceEntry<Post, Blog> referenceEntry2 = context.Entry(post).Reference<Blog>("Blog");
ReferenceEntry referenceEntry3 = context.Entry(post).Reference("Blog");
当用于一对多关系和多对多关系的“多”侧时,导航也可以是相关实体的集合。 Collection 方法用于访问集合导航。 例如:
CollectionEntry<Blog, Post> collectionEntry1 = context.Entry(blog).Collection(e => e.Posts);
CollectionEntry<Blog, Post> collectionEntry2 = context.Entry(blog).Collection<Post>("Posts");
CollectionEntry collectionEntry3 = context.Entry(blog).Collection("Posts");
某些操作对于所有导航都是通用的。 可以使用 EntityEntry.Navigation 方法访问这些操作,以使用引用导航和集合导航。 请注意,同时访问所有导航时,只能使用非通用访问。 例如:
NavigationEntry navigationEntry = context.Entry(blog).Navigation("Posts");
下表概述了使用 ReferenceEntry<TEntity,TProperty>、CollectionEntry<TEntity,TRelatedEntity> 和 NavigationEntry 的方法:
NavigationEntry 成员 | 说明 |
---|---|
MemberEntry.CurrentValue | 获取并设置导航的当前值。 这是集合导航的整个集合。 |
NavigationEntry.Metadata | 导航的 INavigationBase 元数据。 |
NavigationEntry.IsLoaded | 获取或设置一个值,该值指示是否已从数据库完全加载相关实体或集合。 |
NavigationEntry.Load() | 从数据库加载相关实体或集合;请参阅相关数据的显式加载。 |
NavigationEntry.Query() | 查询 EF Core 将用于将此导航加载为可进一步组合的 IQueryable ;请参阅相关数据的显式加载。 |
使用实体的所有属性
EntityEntry.Properties 为实体的每个属性返回 PropertyEntry 的 IEnumerable<T>。 这可用于对实体的每个属性都执行一项操作。 例如,将任何 DateTime 属性设置为 DateTime.Now
:
foreach (var propertyEntry in context.Entry(blog).Properties)
{
if (propertyEntry.Metadata.ClrType == typeof(DateTime))
{
propertyEntry.CurrentValue = DateTime.Now;
}
}
此外,EntityEntry 包含用于同时获取和设置所有属性值的多个方法。 这些方法使用 PropertyValues 类,该类表示属性及其值的集合。 可为当前值或原始值或者为当前存储在数据库中的值获取 PropertyValues。 例如:
var currentValues = context.Entry(blog).CurrentValues;
var originalValues = context.Entry(blog).OriginalValues;
var databaseValues = context.Entry(blog).GetDatabaseValues();
这些 PropertyValues 对象本身并没有多大用处。 但是,可以将它们进行组合以执行操作实体时所需的通用操作。 在使用数据传输对象和解决乐观并发冲突时,这非常有用。 以下部分显示了一些示例。
设置实体或 DTO 中的当前值或原始值
可以通过复制另一个对象中的值来更新实体的当前值或原始值。 例如,假设 BlogDto
数据传输对象 (DTO) 具有与实体类型相同属性的:
public class BlogDto
{
public int Id { get; set; }
public string Name { get; set; }
}
此对象可用于使用 PropertyValues.SetValues 设置跟踪的实体的当前值:
var blogDto = new BlogDto { Id = 1, Name = "1unicorn2" };
context.Entry(blog).CurrentValues.SetValues(blogDto);
使用通过服务调用或 n 层应用程序中的客户端获取的值更新实体时,有时会使用此方法。 请注意,只要使用的对象具有与实体名称匹配的属性,该对象的类型就不必与实体的类型相同。 在以上示例中,DTO BlogDto
的实例用于设置跟踪的 Blog
实体的当前值。
请注意,只有当值集与当前值不同时,才会将属性标记为已修改。
设置字典中的当前值或原始值
上一示例设置实体或 DTO 实例中的值。 当属性值作为名称/值对存储在字典中时,可执行相同的行为。 例如:
var blogDictionary = new Dictionary<string, object> { ["Id"] = 1, ["Name"] = "1unicorn2" };
context.Entry(blog).CurrentValues.SetValues(blogDictionary);
设置数据库中的当前值或原始值
可通过调用 GetDatabaseValues() 或 GetDatabaseValuesAsync 并使用返回的对象设置当前值和/或原始值,使用数据库中的最新值来更新实体的当前值或原始值。 例如:
var databaseValues = context.Entry(blog).GetDatabaseValues();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);
创建包含当前值、原始值或数据库值的克隆对象
从 CurrentValues、OriginalValues 或 GetDatabaseValues 中返回的 PropertyValues 对象可用于使用 PropertyValues.ToObject() 创建实体克隆。 例如:
var clonedBlog = context.Entry(blog).GetDatabaseValues().ToObject();
请注意,ToObject
返回 DbContext 未跟踪的新实例。 返回的对象也未设置与其他实体的任何关系。
克隆的对象有助于解决与数据库的并发更新相关的问题,尤其是在数据绑定到特定类型的对象时。 请参阅乐观并发以了解详细信息。
使用实体的所有导航
EntityEntry.Navigations 为实体的每个导航返回 NavigationEntry 的 IEnumerable<T>。 EntityEntry.References 和 EntityEntry.Collections 执行相同的操作,但分别限于引用导航或集合导航。 这可用于对实体的每个导航都执行一项操作。 例如,强制加载所有相关实体:
foreach (var navigationEntry in context.Entry(blog).Navigations)
{
navigationEntry.Load();
}
使用实体的所有成员
常规属性和导航属性具有不同的状态和行为。 因此,通常分别处理导航和非导航,如上述部分所示。 但是,无论实体的任何成员是常规属性还是导航,对该成员执行某些操作有时是很有用的。 为此提供了 EntityEntry.Member 和 EntityEntry.Members。 例如:
foreach (var memberEntry in context.Entry(blog).Members)
{
Console.WriteLine(
$"Member {memberEntry.Metadata.Name} is of type {memberEntry.Metadata.ClrType.ShortDisplayName()} and has value {memberEntry.CurrentValue}");
}
对示例中的博客运行此代码会生成以下输出:
Member Id is of type int and has value 1
Member Name is of type string and has value .NET Blog
Member Posts is of type IList<Post> and has value System.Collections.Generic.List`1[Post]
提示
更改跟踪器调试视图显示此类信息。 整个更改跟踪器的调试视图从每个跟踪实体的单个 EntityEntry.DebugView 中生成。
Find 和 FindAsync
DbContext.Find、DbContext.FindAsync、DbSet<TEntity>.Find 和 DbSet<TEntity>.FindAsync 设计为在已知主键时高效查找单个实体。 Find 首先检查实体是否已被跟踪,如果是,则立即返回该实体。 只有当未在本地跟踪实体时,才执行数据库查询。 例如,假设此代码对同一实体调用两次 Find:
using var context = new BlogsContext();
Console.WriteLine("First call to Find...");
var blog1 = context.Blogs.Find(1);
Console.WriteLine($"...found blog {blog1.Name}");
Console.WriteLine();
Console.WriteLine("Second call to Find...");
var blog2 = context.Blogs.Find(1);
Debug.Assert(blog1 == blog2);
Console.WriteLine("...returned the same instance without executing a query.");
使用 SQLite 时,此代码的输出(包括 EF Core 日志记录)为:
First call to Find...
info: 12/29/2020 07:45:53.682 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[@__p_0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
SELECT "b"."Id", "b"."Name"
FROM "Blogs" AS "b"
WHERE "b"."Id" = @__p_0
LIMIT 1
...found blog .NET Blog
Second call to Find...
...returned the same instance without executing a query.
请注意,第一次调用未在本地找到实体,因此执行数据库查询。 相反,第二次调用在不查询数据库的情况下返回相同的实例,因为它已被跟踪。
如果具有给定键的实体未在本地进行跟踪并且不存在于数据库中,则 Find 将返回 NULL。
组合键
Find 还可以与组合键结合使用。 例如,假设 OrderLine
实体具有一个由订单 ID 和产品 ID 组成的组合键:
public class OrderLine
{
public int OrderId { get; set; }
public int ProductId { get; set; }
//...
}
必须在 DbContext.OnModelCreating 中配置该组合键,以定义键部分及其顺序。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<OrderLine>()
.HasKey(e => new { e.OrderId, e.ProductId });
}
请注意,OrderId
是键的第一部分,ProductId
是键的第二部分。 将键值传递给 Find 时,必须使用此顺序。 例如:
var orderline = context.OrderLines.Find(orderId, productId);
使用 ChangeTracker.Entries 访问所有跟踪的实体
到目前为止,我们一次只访问了一个 EntityEntry。 ChangeTracker.Entries() 为 DbContext 当前跟踪的每一个实体都返回一个 EntityEntry。 例如:
using var context = new BlogsContext();
var blogs = context.Blogs.Include(e => e.Posts).ToList();
foreach (var entityEntry in context.ChangeTracker.Entries())
{
Console.WriteLine($"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property("Id").CurrentValue}");
}
此代码生成以下输出:
Found Blog entity with ID 1
Found Post entity with ID 1
Found Post entity with ID 2
请注意,会返回博客和帖子的条目。 可以使用 ChangeTracker.Entries<TEntity>() 泛型重载将结果筛选为特定实体类型:
foreach (var entityEntry in context.ChangeTracker.Entries<Post>())
{
Console.WriteLine(
$"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}
此代码的输出显示仅返回帖子:
Found Post entity with ID 1
Found Post entity with ID 2
此外,使用泛型重载返回泛型 EntityEntry<TEntity> 实例。 在此示例中,这就是允许流畅访问 Id
属性的原因。
用于筛选的泛型类型不必为映射实体类型;可以改为使用非映射基类型或接口。 例如,如果模型中的所有实体类型都实现定义其键属性的接口:
public interface IEntityWithKey
{
int Id { get; set; }
}
然后,此接口可用于以强类型方式处理任何跟踪的实体的键。 例如:
foreach (var entityEntry in context.ChangeTracker.Entries<IEntityWithKey>())
{
Console.WriteLine(
$"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}
使用 DbSet.Local 查询跟踪的实体
始终对数据库执行 EF Core 查询,并且此查询仅返回已保存到数据库的实体。 DbSet<TEntity>.Local 提供一种机制,用于查询 DbContext 的本地跟踪的实体。
由于 DbSet.Local
用于查询跟踪的实体,因此通常将实体加载到 DbContext 中,然后使用这些加载的实体。 这尤其适用于数据绑定,但在其他情况下也很有用。 例如,在下面的代码中,首先查询数据库中的所有博客和帖子。 Load 扩展方法用于通过上下文跟踪的结果执行此查询,而无需将结果直接返回到应用程序。 (使用 ToList
或类似的方法具有相同的效果,但具有创建返回列表的开销,这里不需要使用这些方法。)然后,该示例使用 DbSet.Local
访问本地跟踪的实体:
using var context = new BlogsContext();
context.Blogs.Include(e => e.Posts).Load();
foreach (var blog in context.Blogs.Local)
{
Console.WriteLine($"Blog: {blog.Name}");
}
foreach (var post in context.Posts.Local)
{
Console.WriteLine($"Post: {post.Title}");
}
请注意,与 ChangeTracker.Entries() 不同,DbSet.Local
直接返回实体实例。 当然,始终可以通过调用 DbContext.Entry 为返回的实体获取 EntityEntry。
本地视图
DbSet<TEntity>.Local 返回本地跟踪的实体的视图,该视图反映这些实体的当前 EntityState。 具体而言,这表示:
Added
实体包含在内。 请注意,对于普通 EF Core 查询,情况并非如此,因为Added
实体尚不存在于数据库中,因此数据库查询永远不会返回此实体。Deleted
实体排除在外。 请注意,对于普通 EF Core 查询,情况同样并非如此,因为Deleted
实体仍存在于数据库中,因此数据库查询会返回此实体。
所有这些都意味着 DbSet.Local
是关于反映实体图当前概念状态的数据的视图,其中 Added
实体包含在内且 Deleted
实体排除在外。 这与调用 SaveChanges 后预期的数据库状态一致。
这通常是数据绑定的理想视图,因为它根据应用程序所做的更改向用户呈现他们所了解的数据。
以下代码将一个帖子标记为 Deleted
,然后添加一个新帖子并标记为 Added
来演示这一点:
using var context = new BlogsContext();
var posts = context.Posts.Include(e => e.Blog).ToList();
Console.WriteLine("Local view after loading posts:");
foreach (var post in context.Posts.Local)
{
Console.WriteLine($" Post: {post.Title}");
}
context.Remove(posts[1]);
context.Add(
new Post
{
Title = "What’s next for System.Text.Json?",
Content = ".NET 5.0 was released recently and has come with many...",
Blog = posts[0].Blog
});
Console.WriteLine("Local view after adding and deleting posts:");
foreach (var post in context.Posts.Local)
{
Console.WriteLine($" Post: {post.Title}");
}
此代码的输出为:
Local view after loading posts:
Post: Announcing the Release of EF Core 5.0
Post: Announcing F# 5
Post: Announcing .NET 5.0
Local view after adding and deleting posts:
Post: What’s next for System.Text.Json?
Post: Announcing the Release of EF Core 5.0
Post: Announcing .NET 5.0
请注意,已删除的帖子会从本地视图中删除,并且会将添加的帖子包含在其中。
使用 Local 添加和删除实体
DbSet<TEntity>.Local 返回 LocalView<TEntity> 的实例。 这是 ICollection<T> 的实现,可在从集合中添加和删除实体时生成并响应通知。 (这与 ObservableCollection<T> 的概念相同,但实现为对现有 EF Core 更改跟踪条目的投影,而不是实现为独立的集合。)
本地视图的通知连接到 DbContext 更改跟踪,以便本地视图与 DbContext 保持同步。 具体而言:
- 向
DbSet.Local
添加新实体会导致该实体被 DbContext 跟踪,这通常发生在Added
状态下。 (如果该实体已具有生成的键值,则其跟踪状态为Unchanged
。) - 从
DbSet.Local
中删除实体会导致将该实体标记为Deleted
。 - 由 DbContext 跟踪的实体将自动显示在
DbSet.Local
集合中。 例如,如果执行查询以引入更多实体,会自动更新本地视图。 - 标记为
Deleted
的实体将自动从本地集合中删除。
这意味着,只需通过从集合中进行添加和删除,本地视图就可用于操作跟踪的实体。 例如,让我们修改前面的示例代码,以在本地集合中添加和删除帖子:
using var context = new BlogsContext();
var posts = context.Posts.Include(e => e.Blog).ToList();
Console.WriteLine("Local view after loading posts:");
foreach (var post in context.Posts.Local)
{
Console.WriteLine($" Post: {post.Title}");
}
context.Posts.Local.Remove(posts[1]);
context.Posts.Local.Add(
new Post
{
Title = "What’s next for System.Text.Json?",
Content = ".NET 5.0 was released recently and has come with many...",
Blog = posts[0].Blog
});
Console.WriteLine("Local view after adding and deleting posts:");
foreach (var post in context.Posts.Local)
{
Console.WriteLine($" Post: {post.Title}");
}
输出与前面的示例相同,因为对本地视图所做的更改与 DbContext 保持同步。
将本地视图用于 Windows 窗体或 WPF 数据绑定
DbSet<TEntity>.Local 构成了将数据绑定到 EF Core 实体的基础。 但是,Windows 窗体和 WPF 与预期的特定通知集合类型结合使用时效果最佳。 本地视图支持创建这些特定的集合类型:
- LocalView<TEntity>.ToObservableCollection() 为 WPF 数据绑定返回 ObservableCollection<T>。
- LocalView<TEntity>.ToBindingList() 为 Windows 窗体数据绑定返回 BindingList<T>。
例如:
ObservableCollection<Post> observableCollection = context.Posts.Local.ToObservableCollection();
BindingList<Post> bindingList = context.Posts.Local.ToBindingList();
有关使用 EF Core 进行 WPF 数据绑定的详细信息,请参阅 WPF 入门,有关使用 EF Core 进行 Windows 窗体数据绑定的详细信息,请参阅 Windows 窗体入门。
提示
给定 DbSet 实例的本地视图是在首次访问时延迟创建的,然后进行缓存。 创建 LocalView 本身速度很快,并且不会占用大量内存。 但是,它需调用 DetectChanges,这对于大量的实体而言,速度可能很慢。 由 ToObservableCollection
和 ToBindingList
创建的集合也会延迟创建,然后进行缓存。 这两种方法都会创建新的集合,当涉及数千个实体时,创建速度可能会很慢,并且会占用大量内存。