关系导航

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>,则会创建使用 ReferenceEqualityComparerHashSet<T> 实例。
  • 否则,如果导航公开为具有无参数构造函数的具体类型,则会创建该具体类型的实例。 这适用于 List<T>,但也适用于其他集合类型,包括自定义集合类型。
  • 否则,如果导航公开为 IEnumerable<T>ICollection<T>ISet<T>,则会创建使用 ReferenceEqualityComparerHashSet<T> 实例。
  • 否则,如果导航公开为 IList<T>,则会创建 List<T> 实例。
  • 否则会引发异常。

注意

如果使用通知实体(包括更改跟踪代理),则使用 ObservableCollection<T>ObservableHashSet<T> 来代替 List<T>HashSet<T>

重要

更改跟踪文档中所述,EF 仅跟踪具有给定键值的任何实体的单个实例。 这意味着用作导航的集合必须使用引用相等性语义。 默认情况下,将由不重写对象相等性的实体类型获取相应结果。 创建用作导航的 HashSet<T> 时,请务必使用 ReferenceEqualityComparer,以确保它适用于所有实体类型。

配置导航

导航作为配置关系的一部分包含在模型中。 也就是说,按约定或在模型生成 API 中使用 HasOneHasMany 等。 与导航关联的大多数配置都是通过配置关系本身来完成的。

但是,某些类型的配置特定于导航属性本身,而不是整体关系配置的一部分。 这种类型的配置是使用 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();
}