一、概述
Entity Framework Core
允许在模型中使用导航属性来加载关联实体。有三种常见的O/RM模式可用于加载关联数据。
- 预先加载表示从数据库中加载关联数据,作为初始查询的一部分;
- 显示加载表示稍后从数据库中显示加载关联数据;
- 延迟加载表示在访问导航属性时,从数据库中以透明方式加载关联数据;
二、预先加载
可以使用Include
方法来指定要包含在查询结果中的关联数据。如示例,结果中,结果中返回的blogs将使用关联的posts
填充其posts
属性。
using (var context = new BloggingContext()) { var blogs = context.Blogs .Include(blog => blog.Posts) .ToList(); }
Entity Framework Core 会根据之前已加载到上下文实例中的实体自动填充导航属性。 因此,即使不显式包含导航属性的数据,如果先前加载了部分或所有关联实体,则仍可能填充该属性。
可以在单个查询中包含多个关系的关联数据。
using (var context = new BloggingContext()) { var blogs = context.Blogs .Include(blog => blog.Posts) .Include(blog => blog.Owner) .ToList(); }
2.1 包含多个层级
使用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 => post.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(); } • 9
2.2 经过筛选的包含
在应用包含功能来加载相关数据时,可对已包含的集合导航应用某些可枚举的操作,这样就可以对结果进行筛选和排序。
支持的操作包括: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(); }
可对多次包含的每个导航应用相同的操作:
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(); }
三、显示加载
可以通过DbContext.Entry(...)
API显示加载导航属性。
using(var context=new BloggingContext()) { var blog = Context.Blogs.Single(b=>b.BlogId == 1); context.Entry(blog).Collection(b=>b.Posts).Load(); context.Entry(blog) .Reference(b => b.Owner) .Load(); }
还可以通过执行返回关联实体的单独查询来显式加载导航属性。 如果已启用更改跟踪,则在查询具体化实体时,EF Core 将自动设置新加载的实体的导航属性以引用任何已加载的实体,并设置已加载实体的导航属性以引用新加载的实体。
3.1查询关联实体
可以获得表示导航属性内容的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(); }
四、延时加载
使用延迟加载的最简单方法是通过安装Microsoft.EntityFrameworkCore.Proxies
包,并通过调用UseLazyLoadingProxies
来启动该包。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(myConnectionString);
或在使用 AddDbContext 时:
.AddDbContext<BloggingContext>( b => b.UseLazyLoadingProxies() .UseSqlServer(myConnectionString));
EF Core将为可被重写的任何导航属性启用延迟加载。
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; } }
4.1 不使用代理进行延迟加载
不使用代理进行延迟加载的工作方式是将 ILazyLoader
注入到实体中,如实体类型构造函数中所述。例如:
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; } }
此方法不要求实体类型为可继承的类型,也不要求导航属性必须是虚拟的,且允许通过new创建的实体实例在附加到上下文后可进行延迟加载。但它需要对Microsoft.EntityFrameworkCore.Abstractions包中定义的ILazyLoader服务的引用。此包包含所允许的最少的一组类型,以便将依赖此包时所产生的影响将到最低。不过,可以将ILazyLoader.Load方法以委托的形式注入,这样就可以完全避免依赖于实体类型的任何EF Core包。
public class Blog { private ICollection<Post> _posts; public Blog() { } private Blog(Action<object, string> lazyLoader) { LazyLoader = lazyLoader; } private Action<object, string> 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(Action<object, string> lazyLoader) { LazyLoader = lazyLoader; } private Action<object, string> 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; } }
上述代码使用 Load
扩展方法,以便更干净地使用委托:
public static class PocoLoadingExtensions { public static TRelated Load<TRelated>( this Action<object, string> loader, object entity, ref TRelated navigationField, [CallerMemberName] string navigationName = null) where TRelated : class { loader?.Invoke(entity, navigationName); return navigationField; } }