关系导航
EF Core 关系由外键定义。 导航在外键上分层,以提供用于读取和操作关系的自然、面向对象的视图。 通过使用导航,应用程序可以处理实体图,而无需关注外键值出现的情况。
重要
多个关系无法共享导航。 任何外键最多可以关联一个从主体实体到依赖实体的导航,以及最多一个从依赖实体到主体实体的导航。
引用导航
导航有两种形式:引用和集合。 引用导航是对另一个实体的简单对象引用。 它们表示一对多和一对一关系中的“一”方。 例如:
public Blog TheBlog { get; set; }
引用导航必须具有资源库,尽管它不需要是公共资源库。 引用导航不应自动初始化为非空默认值,这样做等于断言不存在的实体存在。
使用 C# 可为空引用类型时,对于可选关系,引用导航必须为 空:
public Blog? TheBlog { get; set; }
必需关系的引用导航可以为空,也可以不为空。
集合导航
集合导航是 .NET 集合类型的实例,即任何实现 ICollection<T> 的类型。 集合包含相关实体类型的实例,其中可以有任何数字。 它们表示引用一对多和多对多关系中的“多”方。 例如:
public ICollection<Post> ThePosts { get; set; }
集合导航不需要具有资源库。 内联初始化集合很常见,因此,如果属性为 null
,则无需检查。 例如:
public ICollection<Post> ThePosts { get; } = new List<Post>();
提示
请勿意外创建表达式 bodied 属性,例如 public ICollection<Post> ThePosts => new List<Post>();
。 每次访问属性时,都会创建一个新的空集合实例,因此无法作为导航使用。
集合类型
基础集合实例必须实现 ICollection<T>,并且必须有一个起作用的 Add
方法。 通常使用 List<T> 或 HashSet<T>。 对于少量的相关实体而言,List<T>
效率很高,并且可保持稳定的排序。 对于大量实体而言,HashSet<T>
的查找效率更高,但排序方面不够稳定。 还可以使用自己的自定义集合实现。
重要
集合必须使用引用相等性。 为集合导航创建 HashSet<T>
时,请确保使用 ReferenceEqualityComparer。
数组不能用于集合导航,因为即使它们实现了 ICollection<T>
,Add
方法在调用时也会引发异常。
即使集合实例必须是 ICollection<T>
,也不需要以这种方式公开集合。 例如,通常会将导航公开为 IEnumerable<T>,用于提供无法由应用程序代码随机修改的只读视图。 例如:
public class Blog
{
public int Id { get; set; }
public IEnumerable<Post> ThePosts { get; } = new List<Post>();
}
此模式的变体包括根据需要操作集合的方法。 例如:
public class Blog
{
private readonly List<Post> _posts = new();
public int Id { get; set; }
public IEnumerable<Post> Posts => _posts;
public void AddPost(Post post) => _posts.Add(post);
}
应用程序代码仍然可以将公开的集合强制转换为 ICollection<T>
,然后对其进行操作。 如果这是一个问题,则实体可以返回集合的防御性副本。 例如:
public class Blog
{
private readonly List<Post> _posts = new();
public int Id { get; set; }
public IEnumerable<Post> Posts => _posts.ToList();
public void AddPost(Post post) => _posts.Add(post);
}
仔细考虑从中获取的值是否足够高,以至超过每次访问导航时创建集合副本的开销。
提示
此最终模式之所以有效,是因为默认情况下,EF 通过其支持字段访问集合。 这意味着 EF 本身从实际集合中添加和移除实体,而应用程序仅与集合的防御性副本交互。
集合导航的初始化
集合导航可由实体类型预先初始化:
public class Blog
{
public ICollection<Post> Posts { get; } = new List<Post>();
}
或事后初始化:
public class Blog
{
private ICollection<Post>? _posts;
public ICollection<Post> Posts => _posts ??= new List<Post>();
}
例如,如果 EF 需要将实体添加到集合导航,则在执行查询时,它将初始化集合(如果当前为 null
)。 创建的实例取决于导航的公开类型。
- 如果导航公开为
HashSet<T>
,则会创建使用 ReferenceEqualityComparer 的HashSet<T>
实例。 - 否则,如果导航公开为具有无参数构造函数的具体类型,则会创建该具体类型的实例。 这适用于
List<T>
,但也适用于其他集合类型,包括自定义集合类型。 - 否则,如果导航公开为
IEnumerable<T>
、ICollection<T>
或ISet<T>
,则会创建使用ReferenceEqualityComparer
的HashSet<T>
实例。 - 否则,如果导航公开为
IList<T>
,则会创建List<T>
实例。 - 否则会引发异常。
注意
如果使用通知实体(包括更改跟踪代理),则使用 ObservableCollection<T> 和 ObservableHashSet<T> 来代替 List<T>
和 HashSet<T>
。
重要
如更改跟踪文档中所述,EF 仅跟踪具有给定键值的任何实体的单个实例。 这意味着用作导航的集合必须使用引用相等性语义。 默认情况下,将由不重写对象相等性的实体类型获取相应结果。 创建用作导航的 HashSet<T>
时,请务必使用 ReferenceEqualityComparer,以确保它适用于所有实体类型。
配置导航
导航作为配置关系的一部分包含在模型中。 也就是说,按约定或在模型生成 API 中使用 HasOne
和 HasMany
等。 与导航关联的大多数配置都是通过配置关系本身来完成的。
但是,某些类型的配置特定于导航属性本身,而不是整体关系配置的一部分。 这种类型的配置是使用 Navigation
方法完成的。 例如,若要强制 EF 通过其属性(而不是使用支持字段)访问导航,请执行以下操作:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Navigation(e => e.Posts)
.UsePropertyAccessMode(PropertyAccessMode.Property);
modelBuilder.Entity<Post>()
.Navigation(e => e.Blog)
.UsePropertyAccessMode(PropertyAccessMode.Property);
}
注意
Navigation
调用不能用于创建导航属性。 它仅用于配置之前通过定义关系或通过约定创建的导航属性。
必需的导航
如果关系是必需的,则需要从依赖实体到主体实体的导航,这反过来又意味着外键属性不可为空。 相反,如果外键可以为空,则导航是可选的,那么关系也是可选的。
从主体实体到依赖实体的引用导航有所不同。 在大多数情况下,主体实体可以在没有任何依赖实体的情况下始终存在。 也就是说,必需的关系并不表示始终存在至少一个依赖实体。 无论是在 EF 模型,还是在关系数据库中,都没有确保主体实体与特定数量的依赖实体相关联的标准方法。 如果需要,则必须在应用程序(业务)逻辑中实现它。
当主体和依赖类型在关系数据库中共享同一个表或包含在文档中时,此规则会出现例外。 共享同一个表的从属类型或非从属类型可能会发生这种情况。 在这种情况下,可以将从主体实体到依赖实体的导航属性标记为必需,表示依赖实体必须存在。
根据需要配置导航属性是使用 Navigation
方法完成的。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Navigation(e => e.BlogHeader)
.IsRequired();
}