文章目录
一、引言
如果完全跟着微软文档来学EF Core,那等学完,项目都凉透了。
光一个创建模型都有十几二十节。
所以应根据实际需要,对重点章节进行学习,一些知识点比较细的、深入的等后续要用到再学。
对我来说,除了基本的CRUD,还有一个很重要的功能就是访问相关联的表。就是点击表上的某个字段,可以跳转查看该字段相关联的表的详细信息。
而我在Query data章节中,发现了Load related data这一节,节中内容与这个功能有很大相关性。
二、 加载关联数据
1. 概述
EF Core允许你在model中使用导航属性来加载关联的实体。下面是三种常用的O/RM模式,用于加载关联数据。
- 贪婪加载,Eager Loading,也叫预先加载,表示相关数据作为初始查询的一部分从数据库加载。
- 显式加载,Explicit Loading,表示稍后从数据库显式加载相关数据。
- 懒汉加载,Lazy Loading,也叫延迟加载,当访问导航属性时,相关数据才从数据库中显示加载。
2. 贪婪加载相关数据
2.1. 贪婪加载
你可以使用Include方法来指定相关数据包含在查询结果中。下面示例,结果中返回的blogs将会使其Posts属性用关联的posts填充。
Include方法的作用可以理解为让查询结果中包含xx数据
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ToList();
}
提示:
EF Core会自动地根据导航属性转到任何其他实体,这些实体会预先加载到上下文实例中。
因此,即使你没有显式地包含导航属性的数据,如果之前加载了一些或所有相关实体,该属性仍然可能被填充。
你可以在单个查询中包含多个关系的关联数据。
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.Include(blog => blog.Owner)
.ToList();
}
警告:
在单个查询中贪婪加载一个集合导航属性可能会带来性能问题。
2.2. 包含多级
你可以使用ThenInclude方法向下挖掘关系,来包含多级的关联数据。
using (var context = new BloggingContext())
{
// 相当于找到更远的关联数据
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ToList();
}
还可以链接多级调用到ThenInclude上,来继续包含更多级的关联数据:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ThenInclude(author => author.Photo)
.ToList();
}
你还可以合并所有调用,以在同一个查询中包含来自多个层级和多个根的相关数据。
using (var context = new BloggingContext())
{
// 合并多个多级查询
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ThenInclude(author => author.Photo)
.Include(blog => blog.Owner)
.ThenInclude(owner => owner.Photo)
.ToList();
}
你也许希望为包含的一个实体包含多个相关实体。
例如,在查询Blogs时,你需要包含Posts,并希望同时包含Posts的Author和Tags。
要使这两者都包含在内,你需要指定从根开始的包含路径。例如,Blog -> Posts -> Author,Blog -> Posts -> Tags。这并不意味着你会得到多余的连接;大多数情况下,EF会在生成SQL时合并这些连接。
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.Include(blog => blog.Posts)
.ThenInclude(post => post.Tags)
.ToList();
}
提示:
你还能使用单个Include方法来加载多个导航。导航指的是a.b这种引用
对于所有引用的导航“链”,或当它们以单个集合结束时,这是可能的。
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Owner.AuthoredPosts)
.ThenInclude(post => post.Blog.Owner.Photo)
.ToList();
}
2.3. 带过滤的包含
注意:
该特性是EF Core 5.0引入的。
当Include应用于加载关联数据时,你可以将某些可枚举的操作添加到所包含的集合导航中,从而允许对结果进行筛选和排序。
支持的操作有:Where、OrderBy、OrderByDescending、ThenBy、ThenByDescending、Skip和Take。
这样的操作应该应用于传给Include方法的lambda中的集合导航,如下面例子:
using (var context = new BloggingContext())
{
var filteredBlogs = context.Blogs
.Include(
blog => blog.Posts
.Where(post => post.BlogId == 1)
.OrderByDescending(post => post.Title)
.Take(5))
.ToList();
}
每个包含的导航只允许一组唯一的筛选器操作。对于给定的集合导航(下例中的blog.Posts)应用多个Include操作,过滤器操作只能指定其中一个:
using (var context = new BloggingContext())
{
var filteredBlogs = context.Blogs
.Include(blog => blog.Posts.Where(post => post.BlogId == 1))
.ThenInclude(post => post.Author)
.Include(blog => blog.Posts)
.ThenInclude(post => post.Tags.OrderBy(postTag => postTag.TagId).Skip(3))
.ToList();
}
相反的是,为每个导航使用同样的操作是可以多次的:
using (var context = new BloggingContext())
{
var filteredBlogs = context.Blogs
.Include(blog => blog.Posts.Where(post => post.BlogId == 1))
.ThenInclude(post => post.Author)
.Include(blog => blog.Posts.Where(post => post.BlogId == 1))
.ThenInclude(post => post.Tags.OrderBy(postTag => postTag.TagId).Skip(3))
.ToList();
}
警告:
在跟踪查询的情况下,由于导航修正,Filtered Include的结果可能是意料之外的。
所有之前查询过并存储在Change Tracker中的相关实体将会呈现在Filtered Include查询的结果中,即使它们不满足过滤器的要求。当在这些情况下使用Filtered Include时,考虑使用NoTracking查询或重新创建上下文。
var orders = context.Orders.Where(o => o.Id > 1000).ToList();
// customer entities will have references to all orders where Id > 1000, rather than > 5000
var filtered = context.Customers.Include(c => c.Orders.Where(o => o.Id > 5000)).ToList();
2.4. 派生类中的Include
你可以使用Include和ThenInclude来包含只定义在派生类中的相关数据。
给定一个model:
public class SchoolContext : DbContext
{
public DbSet<Person> People { get; set; }
public DbSet<School> Schools { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<School>().HasMany(s => s.Students).WithOne(s => s.School);
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Student : Person
{
public School School { get; set; }
}
public class School
{
public int Id { get; set; }
public string Name { get; set; }
public List<Student> Students { get; set; }
}
所有学生的School导航的内容都可以使用多种模式贪婪记载:
- 使用投射:
context.People.Include(person => ((Student)person).School).ToList()
- 使用as操作符:
context.People.Include(person => (person as Student).School).ToList()
- 使用Include的重载来接受一个string类型的参数:
context.People.Include("School").ToList()
2.5. 自动包含导航的模型配置
注意:
该特性是EF Core 6.0引入的。
你可以在模型中配置一个导航,以便每次使用AutoInclude方法从数据库中加载实体时包含该导航。在结果中返回的实体类型
3. 显式加载关联数据
3.1. 显式加载
通过DbContext.Entry(…) API可以显式地加载一个导航属性。
using (var context = new BloggingContext())
{
var blog = context.Blogs // 得到上下文中的Blog实体集合
.Single(b => b.BlogId == 1);// 获取BlogId为1的Blog实体
context.Entry(blog) // 加载blog实体中的导航属性
.Collection(b => b.Posts) // 获取集合导航属性Posts
.Load();
context.Entry(blog) // 加载blog实体中的导航属性
.Reference(b => b.Owner) // 获取引用导航属性Owner
.Load();
}
你还可以通过执行返回关联实体的单独查询来显式加载导航属性。如果启用了变更跟踪(change tracking),则当查询具体化一个实体时,EF Core会自动设置新加载实体的导航属性指向任何已经加载的实体,并设置已经加载的实体的导航属性指向新加载的实体。
3.2. 查询关联实体
你还可以获得一个表示导航属性内容的LINQ查询。
它允许你使用查询上的其他操作符。例如,对关联的实体使用合计操作符,而不需要将它们加载到内存中。
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
var postCount = context.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Count();
}
还可以筛选哪些关联实体加载到内存中,
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
var goodPosts = context.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Where(p => p.Rating > 3)
.ToList();
}
4. 懒汉(延迟)加载关联数据
4.1. 使用代理(proxies)懒汉加载
懒汉加载最简单的方式是通过安装Microsoft.EntityFrameworkCore.Proxies包并且启用它,然后使用UseLazyLoadingProxies调用。例如:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString);
或在使用AddDbContext时:
.AddDbContext<BloggingContext>(
b => b.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString));
EF Core将为任何可以被重写的导航属性启用懒汉加载——就是说,它必须是virtual的并且在一个可被继承的类上(非sealed)。例如,在以下实体中,Post.Blog和Blog.Posts导航属性是懒汉加载的。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public virtual Blog Blog { get; set; }
}
警告
懒汉加载可能会导致不必要的额外数据库往返(所谓的N+1问题.,应该避免这种情况)。
4.2. 不使用代理的懒汉加载
没有代理的懒汉加载通过将==ILazyLoader ==服务注入实体中,正如 Entity Type Constructors中所描述的。例如:
public class Blog
{
private ICollection<Post> _posts;
public Blog()
{
}
private Blog(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts
{
get => LazyLoader.Load(this, ref _posts);
set => _posts = value;
}
}
public class Post
{
private Blog _blog;
public Post()
{
}
private Post(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog
{
get => LazyLoader.Load(this, ref _blog);
set => _blog = value;
}
}