Lazy在Entity Framework中的性能優化實踐(附源碼)

在使用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>來解決這個兼顧代碼設計和性能的問題.

二, 數據表和EF實體介紹

以下圖所示,一共用到3張表:

Student: 學生信息表
StudentScores:學生成績表
StudentHealthies: 學生健康情況表

3張表設計的是1對1關係,現實開發中固然會比這個複雜的多,可是這個對於描述咱們的問題已經足夠了。

table

 

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; }
  }

三, lazy load的性能

下圖中的頁面,讀取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> 

lazy load

四, 使用StudentExtensionRepository來提升效率

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訪問,減小來數據庫的訪問,提交了效率。

2

 

五, 進一步改進,使用StudentExtensionRepository1來實現按需訪問數據庫

上面的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都能提供恰到好處的數據庫訪問。

 

3

 

六, 總結

以上是使用Lazy<T>結合EF的一次性能優化的閉門造車的嘗試,歡迎各位多提意見。若是有更好的方式來,歡迎指教

相關文章
相關標籤/搜索