大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: 垃圾桶 修改讲义

EF默认不会加载entity所关联的entity(navigation properties),比如学生(Student)床铺(SleepIn)的位置(Location):

    Student zl = context.Students.Where(s => s.Name == "zl").Single();

    Console.WriteLine(zl.SleepInId);  //OK
    Console.WriteLine(zl.SleepIn.Location);  //NullReference Exception

否则由于entity之间大量的、链式的、复杂的引用关系,会导致一次性的加载大量entity,耗尽系统内存资源。

但如果所有的关联entity要靠开发人员自己根据其Id查询获取,又显得累赘,所以EF提供了三种加载关联数据的模式:


预先(Eager)加载

关联的数据会随着entity加载同时加载。这需要用到方法:

  • Include():包含当前entity的关联对象,和
  • ThenInclude():包含当前entity关联对象的关联对象,通常用于关联集合entity下的再一次关联。
    早期版本的VSThenInclude()的智能提示可能有问题,略过错误提示直接运行即可
    Teacher fg = context.Teachers.Where(t => t.Name == "fg")    //找到Teacher
        .Include(t => t.Students)    //包含Teacher的Students集合,
            .ThenInclude(s => s.StudyIn)    //再包含Student的StudyIn(Classroom)
        .SingleOrDefault();

如果Student不是集合,可以一直点...比如:

    Teacher fg = context.Teachers.Where(t => t.Name == "fg")
        .Include(t => t.Major.Bases) //Major和Bases都是entity


另外,

  • Include()可以直接并用,即一个entity下Include()多个Property,但
  • ThenInclude()不行(编译器无法区分)

所以,如果需要同时ThenClude()多个Pproperty,不能

    Teacher fg = context.Teachers.Where(t => t.Name == "fg")
        .Include(t => t.Students)
            .ThenInclude(s => s.StudyIn)
            .ThenInclude(s => s.SleepIn)
只能:
    Teacher fg = context.Teachers.Where(t => t.Name == "fg")
        .Include(t => t.Students)
            .ThenInclude(s => s.StudyIn)
        .Include(t => t.Students)   //看似重复,但必须这样写
            .ThenInclude(s => s.SleepIn)

演示:

  • F9断点,Include()之后能看到关联entity的值
  • logger,生成的SQL语句使用JOIN,一次性获取全部数据


track和优化

  1. 一旦被加载,就会被DbContext缓存/追踪(track)
  2. 如果关联数据此前已经被缓存/追踪,那么即使你没有Include(),一样可以被自动关联使用
        Bed bed = context.Find<Bed>(1);  //实现已经将Id=1的Bed加载到DbContext中
        var query = context.Students.Where(s => s.Name == "zl")
            //.Include(s => s.SleepIn) 这里没有Include学生的床铺
  3. 如果最后的结果(Selcet语句)没有使用到Include()里的值,Include会被ignore
        var query = context.Students.Where(s => s.Name == "zl")
            .Include(s => s.SleepIn)
            .Select(s => new { s.Name });  //只取Student.Name,没用到Student.SleepIn

Include中可以强制转换(多态):

      .ThenInclude(t => ((Teacher)t).TeachOn)
      .ThenInclude(t => (t as Teacher).TeachOn)


显式(Explicit)加载

在已经取到一个entity之后,再利用方法:

  • Entry():获取该entity在DbContext中的追踪信息,然后调用其
    之前我们学习过的影子属性,可以用Entry()方法获取
        context.Entry(zl).Property("EnrollDate").CurrentValue = DateTime.Now;
    
  • Reference()/Collection()方法,表明要获取该entity的单个的关联(Reference)或集合(Collection)对象,最后再调用
  • Load():将其加载
    context.Entry(zl)
        .Reference(s => s.StudyIn)
        .Load();

    context.Entry(zl)
        .Collection(s => s.Teachers)
        .Load();
演示:
  • EF分两次用SQL查询分别获取entity信息
  • Load()之后EF自动赋值entity关联属性值


延迟(惰性Lazy)加载

在.NET core 2.1之后支持,需要添加引用Microsoft.EntityFrameworkCore.Proxies,并显式开启:


protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString);
所涉及的entity必须是可被继承的,且关联entity属性必须是可override的(virtual):
    public class Student : Person
    {
        public virtual Bed SleepIn { get; set; }
        public virtual IList<Teacher> Teachers { get; set; }
        public virtual Classroom StudyIn { get; set; }
    }
因为,Lazyload本质上是:


  • 用一个单独的SQL语句获取要加载entity,比如Student
  • 其关联entity是NULL,而是由EF自动新建的一个子类(后缀Proxy)对象,比如SleepInProxy
  • proxy对象只有主键(Id)被赋值,其他属性值为默认值(null,0,false等)
  • 但是,当开发人员视图获取关联entity时,proxy对象会自动连接数据库,获取相关数据,予以加载

演示:


如果是关联的集合,注意使用Query()避免全部加载:
  •             studentRep.Entry(ht)
                    .Collection(m => m.Majors)
                        .Query().Where(m => m.MajorId > 3)
                    .Load();



性能提高


IQueryable

按我们之前的方法,entity引用的集合(collection),都会一次性的把所有集合都取出来。

如果集合的数据量比较大,且我们不需要集合的全部信息,这就是一种性能浪费。比如,我们只想取:

  • 集合元素的个数
  • 集合的某一部分

EF为我们提供了


1+N问题

取出所有教室的所有学生:

    foreach (var room in context.Classrooms
        .ToList() /* 必不可少,否则会有ADO.NET的DataReader错误 */)
    {
        Console.WriteLine(room.Name);
        foreach (var student in room.Students)
        {
            Console.WriteLine($"    {student.Name}");
        }
    }
因为LazyLoad,外个循环里,EF会取出所有的Classrooms,但不会取出Classroom中的Student对象;


内循环里,


补充Include()

            foreach (var room in context.Classrooms.Include(c=>c.Students))


优劣总结:

  • Lazy Load:使用起来最方便,但缺乏精细化控制(至少目前是这样的?)有可能带来1+n的性能问题
  • Explicit Load:会产生多次查询,但可以使用Query()对关联数据进行SQL语句筛选,而且可以重用
  • Eager Load:使用join查询,减少数据库访问次数,但不适合关联大数据的情形(尤其是我们实际上只需要部分关联数据的时候)

飞哥个人偏好:Lazy为主,Eager为附


为什么需要?

而不使用用query查询(Join):保证entity不依赖于repository(database)



EF6的不同

没有ThenInclude,需要Select

    .Include(p => p.KeywordToArticle.Select(ka=>ka.Keyword))
学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 垃圾桶 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码