隐式外键

在一对多关系中,作为 Dependent 的实体类可以配置一个外键,指向 Principal 的主键。实际上这个外键属性是可以省略的,例如将之前定义的 Post 实体改为这样:

class Post
{
    public long Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    //public long BlogId { get; set; }
    public Blog Blog { get; set; }
}

在省略外键属性时,EFCore 依旧会在数据库里创建并管理外键列(因为在数据库层面,一对多关系必须通过外键的形式表示)。Post 实体中已经有 Blog 类型的单体引用了,所以省略掉 BlogId 对访问关系实体并没有太大影响。一个重要的区别在于,如果你只是想获取实体的外键,而不想获取整个 Principal 的话,那么你只能显式定义外键。

在上面省略了 BlogId 的情况下,获取每个 Post 的 BlogId:

using (MyDbContext ctx = new MyDbContext())
{
    var posts = ctx.Posts.Include(e => e.Blog);
    foreach (var post in posts)
    {
        Console.WriteLine(post.Blog.Id);
    }
}

由于没有了外键属性,所以只能 Include 查询,然后再通过导航属性获取 BlogId,生成的 SQL 语句如下:

SELECT [t].[Id], [t].[BlogId], [t].[Content], [t].[Title], [t0].[Id], [t0].[Name]
FROM [T_Posts] AS [t]
LEFT JOIN [T_Blogs] AS [t0] ON [t].[BlogId] = [t0].[Id]

在这里,EFCore 实际上是把整个 Blog 都读取下来了。但我们只是想获取外键,本来 Post 数据库里就有一个外键列,根本不需要用到跨表查询的,因此这里就出现了性能差异了。

注:这里使用隐式外键时,用 Select 语句可以避免将整个 Blog 都读取下来,而只查询 Blog 表的 Id 列,但依旧避免不了跨表查询。

在显式指定外键属性时,对应的查询代码和生成的 SQL 语句如下:

using (MyDbContext ctx = new MyDbContext())
{
    var posts = ctx.Posts;
    foreach (var post in posts)
    {
        Console.WriteLine(post.BlogId);
    }
}
SELECT [t].[Id], [t].[BlogId], [t].[Content], [t].[Title]
FROM [T_Posts] AS [t]

单向导航属性

一个实体可能拥有多个一对多关系,对于一篇文章,可能会有评论表、浏览数据表、归档表等拥有对 Post 的外键引用。此时如果在 Post 实体中都为上述表定义一个导航属性的话就没必要了,因为这些导航属性可能用不上而影响性能,还会导致实体膨胀,这个时候就需要用单向导航属性了。

所谓单向导航属性,只需要在双向导航属性的基础上,省略 Principal 实体中对 Dependent 的集合引用,并省略 WithMany/HasMany 方法的参数即可。

在文章 Post 与每月浏览量 ViewCount 之间的一对多关系:

class ViewCount
{
    public long Id { get; set; }
    public int Count { get; set; }
    public int Month { get; set; }
    public Post Post { get; set; }
}

class Post
{
    public long Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public long BlogId { get; set; }
    public Blog Blog { get; set; }
}

注意到 Post 实体类里并没有一个月浏览量的集合引用,而 ViewCount 有一个对文章的单体引用。这样就形成了单向导航属性,由于没有了集合引用,所以 WithMany/HasMany 的参数就填不了了,省略即可:

public void Configure(EntityTypeBuilder<Post> builder)
{
    builder.HasOne<Blog>(e => e.Blog).WithMany(e => e.Posts);
    builder.HasMany<ViewCount>().WithOne(e => e.Post);
    builder.ToTable("T_Posts");
}

对于主从关系来说,比如一个博客和多篇文章的一对多关系,一般使用双向导航属性,对于其他不明显的一对多关系,一般使用单向导航属性。

上面所说的单向导航属性都是省略集合引用保留单体引用,如果我们省略单体引用保留集合引用,EFCore 能不能识别出来呢?答案是可以的,只要省略 HasOne/WithOne 方法的参数即可。在数据库层面的实现是一样的,EFCore 依旧会生成一个隐式外键,只是在映射时没有了单体引用罢了。集合导航属性则遵循前面所说的,查询外引用需要先 Include。