在使用EF的过程当中,导航属性的lazy load机制,可以减小对数据库的没必要要的访问。只有当你使用到导航属性的时候,才会访问数据库。可是这个只是对于单个实体而言,而不适用于显示列表数据的状况。html
这篇文章介绍的是,使用Lazy<T>来提升显示列表页面的效率。sql
这里是相关的源代码 PerformanceTest.zip数据库
阅读目录:性能优化
1、问题的描述性能
2、数据表和EF实体介绍优化
3、lazy load的性能spa
4、使用StudentExtensionRepository来提升效率设计
5、进一步改进,使用StudentExtensionRepository1来实现按需访问数据库3d
6、总结code
在使用EF的过程当中,导航属性的lazy load机制,可以减小对数据库的没必要要的访问。只有当你使用到导航属性的时候,才会访问数据库。
好比有个学生Student实体,只有当我访问Student的StudentScore(成绩实体)导航属性的时候,才会去访问StudentScore表。
问题是导航属性只是解决了单个实体访问导航属性时候的性能问题,而在实际开发过程当中,经常遇到的问题是,我要显示一个列表的Student的信息,而且每一个Student都要获取StudentScore信息,怎么办?
也许有人会说,可使用EF的eager loading, 在读出Student信息的时候,把StudentScore一块儿读取出来就能够了。
是的,就像下面这样就能解决当前的问题,可是会面临不够通用,没法应对变化的状况。
var students = context.Students .Include(b => b.StudentScore) .ToList();
若是遇到需求,不须要StudentScore信息的时候,咱们就又要改回lazy load的方式.
若是遇到需求,须要显示Student另一个关联属性StudentHealthy信息的时候,又必须让StudentScore用Lazy load方式,而StudentHealthy采用eager loading
下面就介绍如何使用Lazy<T>来解决这个兼顾代码设计和性能的问题.
以下图所示,一共用到3张表:
Student: 学生信息表
StudentScores:学生成绩表
StudentHealthies: 学生健康情况表
3张表设计的是1对1关系,现实开发中固然会比这个复杂的多,可是这个对于描述咱们的问题已经足够了。
EF中对应于着3张表的Model代码:
[Table("Student")] public class Student { [Key] public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public virtual StudentScore Score { get; set; } public virtual StudentHealthy Healthy { get; set; } } public class StudentScore { public int Score { get; set; } public int StudentId { get; set; } [ForeignKey("StudentId")] public virtual Student Student { get; set; } } public class StudentHealthy { public int Score{ get; set; } public int StudentId { get; set; } [ForeignKey("StudentId")] public virtual Student Student { get; set; } }
下图中的页面,读取Student的列表,同时访问Student的导航属性Score来获取成绩信息。
从MiniProfiler能看到,页面一共执行了4个Sql, 其中第一个Sql是从Student表中取出了3条数据,
其他的3个sql是每一个Student访问导航属性而致使的数据库访问.
这里因为Student表中只有3条数据,若是有100条的话,就会致使1+100*3次数据访问,因此这种使用导航属性来获取数据的方式是不适合这种列表数据显示的
这里是页面View的代码:
@model IEnumerable<Student> <h1>Student With Score(Lazy Load)</h1> <table border="1"> <tr><td>ID</td><td>Name</td><td>Age</td><td>Score</td></tr> @foreach(var student in Model) { <tr> <td>@student.Id</td> <td>@student.Name</td> <td>@student.Age</td> <td>@student.Score.Score</td> </tr> } </table>
StudentExtensionRepository解决效率问题的思路是,若是你取出3个student信息的话,它会同时把StudentScore信息取出来。
先来看看StudentExtension的定义
public class StudentExtension { public Student Student { get; set; } public StudentScore StudentScore { get; set; } }
上面的StudentExtension用来保存Student信息,以及和该Student相关的StudentScore数据。
下面是StudentExtensionRepository.cs的具体内容,GetStudents方法中,先取得Student信息,而后获取相关的Score信息。
public class StudentExtensionRepository : IStudentExtensionRepository { private readonly IStudentRepository _studentRepository; private readonly IRepository<StudentScore> _studentScoreRepository; public StudentExtensionRepository(IRepositoryCenter repositoryCenter) { _studentRepository = repositoryCenter.GetRepository<IStudentRepository>(); _studentScoreRepository = repositoryCenter.GetRepository<IRepository<StudentScore>>(); } public IEnumerable<StudentExtension> GetStudents() { var result = new List<StudentExtension>(); var students = _studentRepository.GetStudents().ToList(); //取得score信息 var studentsIds = students.Select(s => s.Id); var scores = _studentScoreRepository.Filter(s => studentsIds.Contains(s.StudentId)).ToList(); foreach (var student in students) { var temp = new StudentExtension { Student = student, StudentScore = scores.FirstOrDefault(s => s.StudentId == student.Id) }; result.Add(temp); } return result; } }
最后,使用新的StudentExtensionRepository,可以发现,显示一样的页面,只用了2个sql访问,减小来数据库的访问,提交了效率。
上面的StudentExtensionRepository的实现,仍是没法解决咱们开始提出的问题:
若是遇到需求,不须要StudentScore信息的时候,咱们就又要改回lazy load的方式.
若是遇到需求,须要显示Student另一个关联属性StudentHealthy信息的时候,又必须让StudentScore用Lazy load方式,而StudentHealthy采用eager loading
如 果咱们要显示Student的另一个关联表StudentHealthy的数据,仍是使用StudentExtensionRepository,会导 致对StudentScore表的访问,可是这是咱们不须要的数据,如何改进StudentExtensionRepository的实现,来达到按需访 问数据库呢?
这个时候,就能够用到Lazy<T>来实现Lazy属性,只有真正用到属性的时候,才会真正的访问数据库。
看看咱们改造后的StudentExtension1类, 该类同时包含了Score和Healthy信息,可是都是Lazy加载的。
public class StudentExtension1 { public Student Student { get; set; } //Lazy属性 public Lazy<StudentScore> StudentScoreLazy { get; set; } //非Lazy属性,从Lazy属性中取值。当真正用到该属性的时候,会触发数据库访问 public StudentScore StudentScore { get { return StudentScoreLazy.Value; } } public Lazy<StudentHealthy> StudentHealthyLazy { get; set; } public StudentHealthy StudentHealthy { get { return StudentHealthyLazy.Value; } } }
改造后的StudentExtensionRepository1
public IEnumerable<StudentExtension1> GetStudents() { var result = new List<StudentExtension1>(); var students = _studentRepository.GetStudents().ToList(); var studentsIds = students.Select(s => s.Id); //存储Score查询的结果,避免屡次访问数据库来获取Score信息 List<StudentScore> scoreListTemp = null; Func<int, StudentScore> getScoreFunc = id => { //第一个Student来获取Score信息的时候,scoreListTemp会是null, 这个时候,会去访问数据库获取Score信息 //第二个以及之后的Student获取Score信息的时候,scoreListTemp已经有值了, 这样就不会再次访问数据库 if (scoreListTemp == null) { scoreListTemp = _studentScoreRepository.Filter( s => studentsIds.Contains(s.StudentId)).ToList(); } return scoreListTemp.FirstOrDefault(s => s.StudentId == id); }; //存储Healthy查询的结果,避免屡次访问数据库来获取Healthy信息 List<StudentHealthy> healthyListTemp = null; Func<int, StudentHealthy> getHealthyFunc = id => { if (healthyListTemp == null) { healthyListTemp = _studentHealthyRepository.Filter( s => studentsIds.Contains(s.StudentId)). ToList(); } return healthyListTemp.FirstOrDefault(s => s.StudentId == id); }; foreach (var student in students) { var id = student.Id; var temp = new StudentExtension1 { Student = student, StudentScoreLazy = new Lazy<StudentScore>(() => getScoreFunc(id)), StudentHealthyLazy = new Lazy<StudentHealthy>(() => getHealthyFunc(id)) }; result.Add(temp); } return result; } }
接下来,建立2个不一样的页面index2和index3, 他们的代码彻底相同,只是View页面中访问的属性不一样。
public ActionResult Index2() { var studentExtensionRepository1 = _repositoryCenter.GetRepository<IStudentExtensionRepository1>(); var students = studentExtensionRepository1.GetStudents().ToList(); return View(students); } public ActionResult Index3() { var studentExtensionRepository1 = _repositoryCenter.GetRepository<IStudentExtensionRepository1>(); var students = studentExtensionRepository1.GetStudents().ToList(); return View(students); }
index2.cshtml中,访问了StudentScore属性
<h1>Student With Score(Use the StudentExtensionRepository1)</h1> <table border="1"> <tr><td>ID</td><td>Name</td><td>Age</td><td>Score</td></tr> @foreach(var student in Model) { <tr> <td>@student.Student.Id</td> <td>@student.Student.Name</td> <td>@student.Student.Age</td> <td>@student.StudentScore.Score</td> </tr> } </table>
index3.cshtml,访问了StudentHealthy属性
<h1>Student With Healthy(Use the StudentExtensionRepository1)</h1> <table border="1"> <tr><td>ID</td><td>Name</td><td>Age</td><td>Healthy</td></tr> @foreach(var student in Model) { <tr> <td>@student.Student.Id</td> <td>@student.Student.Name</td> <td>@student.Student.Age</td> <td>@student.StudentHealthy.Score</td> </tr> } </table>
以下图,尽管Index2和Index3的代码中,获取数据的代码彻底相同,可是实际的访问数据库的sql倒是不一样的。这是因为它们View中的显示数据的需求不一样引发的。改造后的StudentExtensionRepository1可以根据所需来访问数据库, 来减小对于数据库的没必要要的访问。同时它也可以适应更多的状况,不管是只显示Student表数据,仍是要同时显示Student, StudentScore和StudentHealthy数据,StudentExtensionRepository1都能提供恰到好处的数据库访问。
以上是使用Lazy<T>结合EF的一次性能优化的闭门造车的尝试,欢迎各位多提意见。若是有更好的方式来,欢迎指教。