深入了解EntityFramework Core 2.1延迟加载(Lazy Loading)
转自 https://www.cnblogs.com/CreateMyself/p/9195131.html
前言
接下来会陆续详细讲解EF Core 2.1新特性,本节我们来讲讲EF Core 2.1新特性延迟加载,如果您用过EF 6.x就知道滥用延迟加载所带来的灾难,同时呢,对此深知的童鞋到了EF Core中也就造成了极大的心里阴影面积,那么到底该不该用呢?当然,完全取决于您。
如果初学者从未接触过EF 6.x,我们知道EF 6.x默认启用了延迟加载,所以这似乎有点强人所难的意味,在EF Core 2.1对于是否启用延迟加载通过单独提供包的形式来供我们所需,二者相对而言,EF 6.x对于延迟加载的使用是不明确、含糊其辞的,而EF Core 2.1对于延迟加载是很具体、明确的,如此一来至少不会造成滥用的情况。
深入理解EF Core 2.1延迟加载
在我个人公众号发过一篇文章也通过示例讲解了EF Core延迟加载的雏形早就有了,这里再稍微给个示例解释EF Core 2.1之前延迟加载的影子在哪里呢?我们依然给出已经用烂了的两个Blog和Post这两个类,如下:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime ModifiedTime { get; set; }
public byte Status { get; set; }
public bool IsDeleted { get; set; }
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime ModifiedTime { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
接下来我们进行如下查询,我们能够看到此时并未利用Include显式加载Posts,通过如下查询EF Core内部会进行关系修正从而查询出Posts。
var dbContext = new EFCoreDbContext();
var blog = dbContext.Blogs.FirstOrDefault();
var posts = dbContext.Posts.Where(d => d.Blog.Id == blog.Id).ToList();
在EF Core 2.1中我们需要下载【Microsoft.EntityFrameworkCore.Proxies】包,同时在OnConfiguring方法中配置启用延迟加载代理,如下:
除了通过如上配置外导航属性必须用virtual关键字修饰(如上示例类未添加,请自行添加),否则将抛出异常,你懂的。接下来在完全配置好延迟加载的前提下再来进行如下查询,此时在我们需要用到Posts时才会去数据库中查询。
var dbContext = new EFCoreDbContext();
var blog = dbContext.Blogs.FirstOrDefault();
var posts = blog.Posts.ToList();
对于EF Core中的延迟加载和EF 6.x使用方式无异,接下来我们再来看看官网给出了未启用代理也可进行延迟加载,通过安装【Microsoft.EntityFrameworkCore.Abstractions 】包引用ILazyLoader服务进行实现,如下:
public class Blog
{
private ILazyLoader LazyLoader { get; set; }
public Blog(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime ModifiedTime { get; set; }
public byte Status { get; set; }
public bool IsDeleted { get; set; }
private ICollection<Post> _posts;
public ICollection<Post> Posts
{
get => LazyLoader?.Load(this, ref _posts);
set => _posts = value;
}
}
public class Post
{
private ILazyLoader LazyLoader { get; set; }
public Post(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime ModifiedTime { get; set; }
public int? BlogId { get; set; }
private Blog _blog;
public Blog Blog
{
get => LazyLoader?.Load(this, ref _blog);
set => _blog = value;
}
}
通过如上注入ILazyLoader服务依附于实体上最终实现延迟加载,这么做对于启用延迟加载代理的最大好处在于只针对特定实体进行延迟加载,而使用延迟加载代理则是全局配置,所有实体都必须通过virtual关键字修饰且都会实现延迟加载。同时呢,通过如下图可知,此时ILazyLoader依附在上下文中也就是说一旦创建实体实例将实现延迟加载。
当然无论是EF 6.x还是EF Core 2.1进行如下查询,那么结果对于如下第二次查询的导航属性将不会再去数据库中查询,因为主体对应的导航属性在内存中已存在,此时将直接返回,也就是说主体查询两次,而依赖实体则将只执行一次查询。
var dbContext = new EFCoreDbContext();
var blog = dbContext.Blogs.FirstOrDefault();
var posts = blog.Posts.ToList();
var blog1 = dbContext.Blogs.FirstOrDefault();
var posts1 = blog1.Posts.ToList();
接下来我们再来看看在上下文实例池中启用延迟加载并结合显式加载看看EF Core执行策略是怎样的呢?
var services = new ServiceCollection();
services.AddDbContextPool<EFCoreDbContext>(options =>
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddConsole(LogLevel.Debug);
options.UseLazyLoadingProxies().UseLoggerFactory(loggerFactory).UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=EFCore2xDb;integrated security=True;MultipleActiveResultSets=True;");
});
var serviceProvider = services.BuildServiceProvider();
var dbContext = serviceProvider.GetRequiredService<EFCoreDbContext>();
var blog = dbContext.Blogs.Include(d => d.Posts).Last();
var posts = blog.Posts.FirstOrDefault();
var blog1 = dbContext.Blogs.FirstOrDefault();
var posts1 = blog1.Posts.FirstOrDefault();
上述查询并结合最终生成的SQL得知:当没有执行显式加载时若我们启用延迟加载则强制执行延迟加载,否则执行显式加载,同时我们通过验证也得知注入ILazyLoader服务后并调用Load方法加载关联实体内部本质则是若要访问的关联实体在内存中不存在时则执行RPC加载关联实体,否则将从内存中获取。
如上所述若我们没有显式进行饥饿加载,那么在启用延迟加载的前提下对于任何关联实体是否都会执行延迟加载呢?比如复杂属性呢?我们再来看看如下示例(请自行在上述配置上下文实例池中启用延迟加载代理)。
public class StreetAddress
{
public string Street { get; set; }
public string City { get; set; }
}
public class Order
{
public int Id { get; set; }
public virtual StreetAddress ShippingAddress { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(e =>
{
e.ToTable("Orders");
e.HasKey(k => k.Id);
e.OwnsOne(o => o.ShippingAddress);
});
}
var order = dbContext.Orders.FirstOrDefault();
var shippingAddress = order.ShippingAddress;
我们从如上图生成的SQL得知:对于复杂属性即通过OwnOne配置的复杂属性在查询时,EF Core会一次性将复杂属性值全部返回(也就是说复杂属性值总是会加载),所以对于启用延迟加载不会起到任何作用,即使是通过Include进行显式加载都是多此一举。
延迟加载避免循环引用
无论是EF 6.x还是EF Core 2.1中的延迟加载都无法避免循环引用问题,若在.NET Core Web应用程序中启用了延迟加载,我们需要在ConfigureServices方法中进行如下配置才行。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
}
在写博客期间有一些童鞋在评论中留言问我是如何学习的,每个人学习方式肯定不一样,但是绝对少不了多敲代码,这里就有一个实际例子(这里不是批评那位童鞋哈),有一位童鞋购买了我写的书(首先感谢那位童鞋),看到某一章节问我那里是不是错了,我一看大概就问了那位童鞋是不是没敲代码,个人觉得无论是看技术书还是看技术博客,还是要敲一遍为好,因为那个章节的某一个类是在某一命名空间下的,估计那位童鞋没见过所以以为错了,别光顾着看,多敲敲代码验证验证总没错的。