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


在使用EF的過程中,導航屬性的lazy load機制,能夠減少對數據庫的不必要的訪問。只有當你使用到導航屬性的時候,才會訪問數據庫。但是這個只是對於單個實體而言,而不適用於顯示列表數據的情況。

這篇文章介紹的是,使用Lazy<T>來提高顯示列表頁面的效率。

這里是相關的源代碼 PerformanceTest.zip

閱讀目錄:

一、問題的描述

二、數據表和EF實體介紹

三、lazy load的性能

四、使用StudentExtensionRepository來提高效率

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

六、總結

一,問題的描述

在使用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的一次性能優化的閉門造車的嘗試,歡迎各位多提意見。如果有更好的方式來,歡迎指教


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM