隐式外键
在一对多关系中,作为 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。