加载相关数据Loading Related Data

Entity Framework Core 允许你在模型中使用导航属性来加载相关实体。 有三种常见的 O/RM 模式可用于加载关联数据。

  • 预先加载表示从数据库中加载关联数据,作为初始查询的一部分。
  • 显式加载表示稍后从数据库中显式加载关联数据。
  • 延迟加载表示在访问导航属性时,从数据库中以透明方式加载关联数据。

提示

可在 GitHub 上查看此文章的示例

预先加载Eager loading

可以使用 Include 方法来指定要包含在查询结果中的关联数据。 在以下示例中,结果中返回的blogs将使用关联的posts填充其 Posts 属性。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .ToList();
  6. }

提示

Entity Framework Core 会根据之前已加载到上下文实例中的实体自动填充导航属性。 因此,即使不显式包含导航属性的数据,如果先前加载了部分或所有关联实体,则仍可能填充该属性。

可以在单个查询中包含多个关系的关联数据。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .Include(blog => blog.Owner)
  6. .ToList();
  7. }

包含多个层级Including multiple levels

使用 ThenInclude 方法可以依循关系包含多个层级的关联数据。 以下示例加载了所有博客、其关联文章及每篇文章的作者。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .ThenInclude(post => post.Author)
  6. .ToList();
  7. }

可通过链式调用 ThenInclude,进一步包含更深级别的关联数据。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .ThenInclude(post => post.Author)
  6. .ThenInclude(author => author.Photo)
  7. .ToList();
  8. }

可以将来自多个级别和多个根的关联数据合并到同一查询中。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .ThenInclude(post => post.Author)
  6. .ThenInclude(author => author.Photo)
  7. .Include(blog => blog.Owner)
  8. .ThenInclude(owner => owner.Photo)
  9. .ToList();
  10. }

你可能希望将已包含的某个实体的多个关联实体都包含进来。 例如,当查询 Blogs 时,你会包含 Posts,然后希望同时包含 PostsAuthorTags。 为此,需要从根级别开始指定每个包含路径。 例如,Blog -> Posts -> AuthorBlog -> Posts -> Tags。 这并不意味着会获得冗余联接查询,在大多数情况下,EF 会在生成 SQL 时合并相应的联接查询。

  1. using (var context = new BloggingContext())
  2. {
  3. var blogs = context.Blogs
  4. .Include(blog => blog.Posts)
  5. .ThenInclude(post => post.Author)
  6. .Include(blog => blog.Posts)
  7. .ThenInclude(post => post.Tags)
  8. .ToList();
  9. }

注意

从版本 3.0.0 开始,每个 Include 都将导致向关系提供程序生成的 SQL 查询添加额外的 JOIN,而以前的版本则生成其他 SQL 查询。 这可以显著地改变(提升或降低)查询性能。 具体而言,具有大量 Include 运算符的 LINQ 查询可能需要将分解为多个单独的 LINQ 查询,以避免笛卡尔爆炸问题。

经过筛选的包含Filtered include

备注

EF Core 5.0 中已引入了此功能。

在应用包含功能来加载相关数据时,可对已包含的集合导航应用某些可枚举的操作,这样就可对结果进行筛选和排序。

支持的操作包括:WhereOrderByOrderByDescendingThenByThenByDescendingSkipTake

应对传递到 Include 方法的 Lambda 中的集合导航应用这类操作,如下例所示:

  1. using (var context = new BloggingContext())
  2. {
  3. var filteredBlogs = context.Blogs
  4. .Include(blog => blog.Posts
  5. .Where(post => post.BlogId == 1)
  6. .OrderByDescending(post => post.Title)
  7. .Take(5))
  8. .ToList();
  9. }

只能对每个包含的导航执行一组唯一的筛选器操作。 如果为某个给定的集合导航应用了多个包含操作(下例中为 blog.Posts),则只能对其中一个导航指定筛选器操作:

  1. using (var context = new BloggingContext())
  2. {
  3. var filteredBlogs = context.Blogs
  4. .Include(blog => blog.Posts.Where(post => post.BlogId == 1))
  5. .ThenInclude(post => post.Author)
  6. .Include(blog => blog.Posts)
  7. .ThenInclude(post => post.Tags.OrderBy(postTag => postTag.TagId).Skip(3))
  8. .ToList();
  9. }

或者,可对多次包含的每个导航应用相同的操作:

  1. using (var context = new BloggingContext())
  2. {
  3. var filteredBlogs = context.Blogs
  4. .Include(blog => blog.Posts.Where(post => post.BlogId == 1))
  5. .ThenInclude(post => post.Author)
  6. .Include(blog => blog.Posts.Where(post => post.BlogId == 1))
  7. .ThenInclude(post => post.Tags.OrderBy(postTag => postTag.TagId).Skip(3))
  8. .ToList();
  9. }

派生类型上的包含Include on derived types

可以使用 IncludeThenInclude 包括来自仅在派生类型上定义的导航的相关数据。

给定以下模型:

  1. public class SchoolContext : DbContext
  2. {
  3. public DbSet<Person> People { get; set; }
  4. public DbSet<School> Schools { get; set; }
  5. protected override void OnModelCreating(ModelBuilder modelBuilder)
  6. {
  7. modelBuilder.Entity<School>().HasMany(s => s.Students).WithOne(s => s.School);
  8. }
  9. }
  10. public class Person
  11. {
  12. public int Id { get; set; }
  13. public string Name { get; set; }
  14. }
  15. public class Student : Person
  16. {
  17. public School School { get; set; }
  18. }
  19. public class School
  20. {
  21. public int Id { get; set; }
  22. public string Name { get; set; }
  23. public List<Student> Students { get; set; }
  24. }

所有人员(可以使用许多模式预先加载的学生)的 School 导航的内容:

  • 使用强制转换

    1. context.People.Include(person => ((Student)person).School).ToList()
  • 使用 as 运算符

    1. context.People.Include(person => (person as Student).School).ToList()
  • 使用 Include 的重载,该方法采用 string 类型的参数

    1. context.People.Include("School").ToList()

显式加载Explicit loading

可以通过 DbContext.Entry(...) API 显式加载导航属性。

  1. using (var context = new BloggingContext())
  2. {
  3. var blog = context.Blogs
  4. .Single(b => b.BlogId == 1);
  5. context.Entry(blog)
  6. .Collection(b => b.Posts)
  7. .Load();
  8. context.Entry(blog)
  9. .Reference(b => b.Owner)
  10. .Load();
  11. }

还可以通过执行返回关联实体的单独查询来显式加载导航属性。 如果已启用更改跟踪,则在加载实体时,EF Core 将自动设置新加载的实体的导航属性以引用任何已加载的实体,并设置已加载实体的导航属性以引用新加载的实体。

查询相关实体Querying related entities

还可以获得表示导航属性内容的 LINQ 查询。

这样可以执行诸如通过相关实体运行聚合运算符而无需将其加载到内存中等操作。

  1. using (var context = new BloggingContext())
  2. {
  3. var blog = context.Blogs
  4. .Single(b => b.BlogId == 1);
  5. var postCount = context.Entry(blog)
  6. .Collection(b => b.Posts)
  7. .Query()
  8. .Count();
  9. }

还可以筛选要加载到内存中的关联实体。

  1. using (var context = new BloggingContext())
  2. {
  3. var blog = context.Blogs
  4. .Single(b => b.BlogId == 1);
  5. var goodPosts = context.Entry(blog)
  6. .Collection(b => b.Posts)
  7. .Query()
  8. .Where(p => p.Rating > 3)
  9. .ToList();
  10. }

延迟加载Lazy loading

使用延迟加载的最简单方式是通过安装 Microsoft.EntityFrameworkCore.Proxies 包,并通过调用 UseLazyLoadingProxies 来启用该包。 例如:

  1. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  2. => optionsBuilder
  3. .UseLazyLoadingProxies()
  4. .UseSqlServer(myConnectionString);

或在使用 AddDbContext 时:

  1. .AddDbContext<BloggingContext>(
  2. b => b.UseLazyLoadingProxies()
  3. .UseSqlServer(myConnectionString));

EF Core 接着会为可重写的任何导航属性(即,必须是 virtual 且在可被继承的类上)启用延迟加载。 例如,在以下实体中,Post.BlogBlog.Posts 导航属性将被延迟加载。

  1. public class Blog
  2. {
  3. public int Id { get; set; }
  4. public string Name { get; set; }
  5. public virtual ICollection<Post> Posts { get; set; }
  6. }
  7. public class Post
  8. {
  9. public int Id { get; set; }
  10. public string Title { get; set; }
  11. public string Content { get; set; }
  12. public virtual Blog Blog { get; set; }
  13. }

不使用代理的延迟加载Lazy loading without proxies

使用代理进行延迟加载的工作方式是将 ILazyLoader 注入到实体中,如实体类型构造函数中所述。 例如:

  1. public class Blog
  2. {
  3. private ICollection<Post> _posts;
  4. public Blog()
  5. {
  6. }
  7. private Blog(ILazyLoader lazyLoader)
  8. {
  9. LazyLoader = lazyLoader;
  10. }
  11. private ILazyLoader LazyLoader { get; set; }
  12. public int Id { get; set; }
  13. public string Name { get; set; }
  14. public ICollection<Post> Posts
  15. {
  16. get => LazyLoader.Load(this, ref _posts);
  17. set => _posts = value;
  18. }
  19. }
  20. public class Post
  21. {
  22. private Blog _blog;
  23. public Post()
  24. {
  25. }
  26. private Post(ILazyLoader lazyLoader)
  27. {
  28. LazyLoader = lazyLoader;
  29. }
  30. private ILazyLoader LazyLoader { get; set; }
  31. public int Id { get; set; }
  32. public string Title { get; set; }
  33. public string Content { get; set; }
  34. public Blog Blog
  35. {
  36. get => LazyLoader.Load(this, ref _blog);
  37. set => _blog = value;
  38. }
  39. }

这不要求实体类型为可继承的类型,也不要求导航属性必须是虚拟的,且允许通过 new 创建的实体实例在附加到上下文后可进行延迟加载。 但它需要对 Microsoft.EntityFrameworkCore.Abstractions 包中定义的 ILazyLoader 服务的引用。 此包包含所允许的最少的一组类型,以便将依赖此包时所产生的影响降至最低。 不过,可以将 ILazyLoader.Load 方法以委托的形式注入,这样就可以完全避免依赖于实体类型的任何 EF Core 包。 例如:

  1. public class Blog
  2. {
  3. private ICollection<Post> _posts;
  4. public Blog()
  5. {
  6. }
  7. private Blog(Action<object, string> lazyLoader)
  8. {
  9. LazyLoader = lazyLoader;
  10. }
  11. private Action<object, string> LazyLoader { get; set; }
  12. public int Id { get; set; }
  13. public string Name { get; set; }
  14. public ICollection<Post> Posts
  15. {
  16. get => LazyLoader.Load(this, ref _posts);
  17. set => _posts = value;
  18. }
  19. }
  20. public class Post
  21. {
  22. private Blog _blog;
  23. public Post()
  24. {
  25. }
  26. private Post(Action<object, string> lazyLoader)
  27. {
  28. LazyLoader = lazyLoader;
  29. }
  30. private Action<object, string> LazyLoader { get; set; }
  31. public int Id { get; set; }
  32. public string Title { get; set; }
  33. public string Content { get; set; }
  34. public Blog Blog
  35. {
  36. get => LazyLoader.Load(this, ref _blog);
  37. set => _blog = value;
  38. }
  39. }

上述代码使用 Load 扩展方法,以便更干净地使用委托:

  1. public static class PocoLoadingExtensions
  2. {
  3. public static TRelated Load<TRelated>(
  4. this Action<object, string> loader,
  5. object entity,
  6. ref TRelated navigationField,
  7. [CallerMemberName] string navigationName = null)
  8. where TRelated : class
  9. {
  10. loader?.Invoke(entity, navigationName);
  11. return navigationField;
  12. }
  13. }

备注

延迟加载委托的构造函数参数必须名为“lazyLoader”。 未来的一个版本中的配置将计划采用另一个名称。

关联数据和序列化Related data and serialization

由于 EF Core 会自动修正导航属性,因此在对象图中可能会产生循环引用。 例如,加载博客及其关联文章会生成引用文章集合的博客对象。 而其中每篇文章又会引用该博客。

某些序列化框架不允许使用循环引用。 例如,Json.NET 在产生循环引用的情况下,会引发以下异常。

Newtonsoft.Json.JsonSerializationException:为“MyApplication.Models.Blog”类型的“Blog”属性检测到自引用循环。

如果正在使用 ASP.NET Core,则可以将 Json.NET 配置为忽略在对象图中找到的循环引用。 这是在 Startup.cs 中通过 ConfigureServices(...) 方法实现的。

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. ...
  4. services.AddMvc()
  5. .AddJsonOptions(
  6. options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
  7. );
  8. ...
  9. }

另一种方法是使用 [JsonIgnore] 特性修饰其中一个导航属性,该特性指示 Json.NET 在序列化时不遍历该导航属性。