EF 延迟加载和预先加载
最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来。
十年河东十年河西,莫欺少年穷
学无止境,精益求精
本节探讨延迟加载和预先加载
Entity Framework作为一个优秀的ORM框架,它使得操作数据库就像操作内存中的数据一样,但是这种抽象是有性能代价的,故鱼和熊掌不能兼得。但是,通过对EF的学习,可以避免不必要的性能损失。本篇只介绍关联实体的加载的相关知识,这在我之前的文章中都有介绍。
我们已经了解到EF的关联实体加载有三种方式:Lazy Loading,Eager Loading,Explicit Loading,其中Lazy Loading和Explicit Loading都是延迟加载。Eager Loading是预先加载
延迟加载(Lazy Loading)。当实体第一次被读取时,相关数据不会被获取。但是,当你第一次尝试存取导航属性时,该导航属性所需的数据会自动加载。结果会使用多个查询发送到数据库——一次是读取实体本身,然后是每个相关的实体。DbContext类默认是使用延迟加载的。
使用延迟加载必须满足以下两个条件
1、类是由Public修饰,不能是封闭类,也就是说,不能带有Sealded修饰符
2、导航属性标记为Virtual。
如下 Score 模型满足了延迟加载的两个条件
public class Score { [Key] public int Id { get; set; } public int StudentScore { get; set; }//学生分数 public int StudentID { get; set; }//学生ID public int CourseID { get; set; }//课程ID public virtual Student Student { get; set; }//virtual关键字修饰,用于延迟加载 提高性能 只有显式调用时 才会加载 并可以代表一个Student对象 也就是 属性==对象 public virtual Course Course { get; set; }//virtual关键字修饰,用于延迟加载 提高性能 只有显式调用时 才会加载 并可以代表一个Course对象 也就是 属性==对象 }
如果您不想使用延迟加载,您可以关闭Lazy Loading,将LazyLoadingEnabled设为false,如果导航属性没有标记为virtual,Lazy Loading也是不起作用的。
public StudentContext() : base("StudentContext")//指定连接字符串 { this.Configuration.LazyLoadingEnabled = false; //关闭延迟加载 }
(一)延迟加载
延迟加载就是数据不会一次性查出来,而是一条一条的查询,这样就会多次请求数据库进行查询,增加了数据库的负担。如果您的数据量打太大,建议使用预先加载,如果两张表数据量很大,建议使用延迟加载。
public ActionResult linqTOsql() { using (var db = new StudentContext()) { var Elist = db.Scores.Where(t => t.StudentScore > 80); foreach (Score item in Elist) { //todo 延迟加载 执行多次查询 } } return View(); }
(二)预先加载<Eager Loading>使用Include方法关联预先加载的实体。
注:需引入:using System.Data.Entity;命名空间
预先加载就是从数据库中一次性查询所有数据,存放到内存中。如下方法采用预先加载
public ActionResult linqTOsql() { using (var db = new StudentContext()) { var Elist = from o in db.Scores.Include(t=>t.Student).Where(t=>t.StudentScore>80) select o; ViewBag.Elist = Elist; } return View(); }
上述代码就是将Score表和Student表左连接,然后查询学生成绩在80分以上的信息。Include()是立即查询的,像ToList()一样,不会稍后延迟优化后再加载。
如果两张表记录很大(字段多,上百万条记录),采用Include()关联两张表效率会很低,因为:它除了要做笛卡尔积,还要把数据一次性查询出来。因此:在字段多,记录多的情况下,建议使用延迟加载。
在此需要说明的是:EF中有两种表关联的方法,一种是Join()方法,一种是Include()方法
Join()方法使用说明:两表不必含有外键关系,需要代码手动指定连接外键相等(具有可拓展性,除了值相等,还能指定是>,<以及其他对两表的相应键的关系),以及结果字段。
Include()方法说明:两表必须含有外键关系,只需要指定键名对应的类属性名即可,不需指定结果字段(即全部映射)。默认搜索某表时,不会顺带查询外键表,直到真正使用时才会再读取数据库查询;若是使用 Include(),则会在读取本表时把指定的外键表信息也读出来。
(三)显式加载<Explicit Loading>有点类似于延迟加载,只是你在代码中显式地获取相关数据。当您访问一个导航属性时,它不会自动加载。你需要通过使用实体的对象状态管理器并调用集合上的Collection.Load方法或通过持有单个实体的属性的Reference.Load方法来手动加载相关数据。
数据模型更改如下:
public class Score { [Key] public int Id { get; set; } public int StudentScore { get; set; }//学生分数 public int StudentID { get; set; }//学生ID public int CourseID { get; set; }//课程ID //变成了泛型 public virtual List<Student> Student { get; set; }//virtual关键字修饰,用于延迟加载 提高性能 只有显式调用时 才会加载 并可以代表一个Student对象 也就是 属性==对象 public virtual Course Course { get; set; }//virtual关键字修饰,用于延迟加载 提高性能 只有显式调用时 才会加载 并可以代表一个Course对象 也就是 属性==对象 }
代码如下:
public ActionResult linqTOsql() { using (var db = new StudentContext()) { var Elist = db.Scores.Where(t => t.StudentScore > 80); foreach (Score item in Elist) { var model = db.Entry(item);//Entry: 获取给定实体的 System.Data.Entity.Infrastructure.DbEntityEntry<TEntity> 对象,以便提供对与该实体有关的信息的访问以及对实体执行操作的功能。 model.Collection(t => t.Student).Load();//Score中的Student导航属性 必须为泛型集合 查询/加载实体集合 foreach (Student A in item.Student) { } } } return View(); }
性能注意事项
如果你知道你立即需要每个实体的相关数据,预先加载通常提供最佳的性能。因为单个查询发送到数据库并一次性获取数据的效率通常比在每个实体上再发出一次查询的效率更高。例如,在上面的示例中,假定每个系有十个相关的课程,预先加载会导致只有一个查询(join联合查询)往返于数据库。延迟加载和显式加载两者都将造成11个查询和往返。在高延迟的情况下,额外的查询和往返通常是不利的。
另一方面,在某些情况下使用延迟加载的效率更高。预先加载可能会导致生成SQL Server不能有效处理的非常复杂的联接查询。或者,如果您正在处理的是需要访问的某个实体的导航属性,该属性仅为实体集的一个子集,延迟加载可能比预先加载性能更好,因为预先加载会将所有的数据全部加载,即使你不需要访问它们。如果应用程序的性能是极为重要的,你最好测试并在这两种方法之间选择一种最佳的。
延迟加载可能会屏蔽一些导致性能问题的代码。例如,代码没有指定预先或显式加载但在处理大量实体并时在每次迭代中都使用了导航属性的情况下,代码的效率可能会很低(因为会有大量的数据库往返查询)。一个在开发环境下表现良好的应用程序可能会在移动到Windows Azure SQL数据库时由于增加了延迟导致延迟加载的性能下降。你应当分析并测试以确保延迟加载是否是适当的
在EF中表连接常用的有Join()和Include(),两者都可以实现两张表的连接,但又有所不同。
例如有个唱片表Album(AlbumId,Name,CreateDate,GenreId),表中含外键GenreId连接流派表Genre(GenreId,Name)。每个唱片归属唯一一个流派,一个流派可以对应多个唱片。
1.Join(),两表不必含有外键关系,需要代码手动指定连接外键相等(具有可拓展性,除了值相等,还能指定是>,<以及其他对两表的相应键的关系),以及结果字段。
重载方式(是扩展方法,第一个参数带this,代表自身):
1.public static IQueryable<TResult> Join<TOuter, TInner, TKey, TResult>(this IQueryable<TOuter> outer, IEnumerable<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector, Expression<Func<TInner, TKey>> innerKeySelector, Expression<Func<TOuter, TInner, TResult>> resultSelector); 2.public static IQueryable<TResult> Join<TOuter, TInner, TKey, TResult>(this IQueryable<TOuter> outer, IEnumerable<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector, Expression<Func<TInner, TKey>> innerKeySelector, Expression<Func<TOuter, TInner, TResult>> resultSelector, IEqualityComparer<TKey> comparer);
那么可以这么写两个表的连接:
var wholeRecord = dc.Album.Join(dc.Genre, a => a.GenreId, g => g.GenreId, (a, g) => new { a.AlbumId,a.Name,g.GenreId,g.Name;
这样就选取除了两表的AlbumId,Name,GenreId,Name。
2.Include(),两表必须含有外键关系,只需要指定键名对应的类属性名即可,不需指定结果字段(即全部映射)。默认搜索某表时,不会顺带查询外键表,直到真正使用时才会再读取数据库查询;若是使用 Include(),则会在读取本表时把指定的外键表信息也读出来。
重载方式:
//位于namespace System.Data.Entity.Infrastructure public DbQuery<TResult> Include(string path); //位于namespace System.Data.Entity,务必引入才能找到该方法。否则只看到上个方法 public static IQueryable<T> Include<T, TProperty>(this IQueryable<T> source, Expression<Func<T, TProperty>> path) where T : class; public static IQueryable<T> Include<T>(this IQueryable<T> source, string path) where T : class;
可以这么写:
//EF已经生成了Album和Genre的数据库映射模型类以及导航属性 var wholeRecord=dc.Album.Include("Genre"); //或者 //var wholeRecord=dc.Album.Include(a=>Genre);
这样数据库就执行了一个左连接,把Album和Genre的所有字段全部连起来了,并且Include()是立即查询的,像ToList()一样,不会稍后延迟优化后再加载。
这样其实效率很低,因为如果两张表记录很大,那么连接是个费时费资源的事情,建议少用,或者先筛选出需要的结果集再连接。