程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 性能優化總結(六):預加載、聚合SQL應用實例

性能優化總結(六):預加載、聚合SQL應用實例

編輯:關於.NET

前面已經把原理都講了一遍,這篇主要是給出一個應用的實例。該實例取自GIX4,比較復雜。

領域模型:

領域模型間的關系,如下:

右邊模型鏈的具體關系在《第二篇》中已經描述過,不再贅述。

本次重點在於紅線框住部分:

Project:表示一個建設項目;

ProjectPBS:一個項目下包含的很多PBS;

PBSPropertyValue:一個PBS我們可以為它設置多個值,每一個值對應一個PBSType(模板)中已定義的屬性,值的范圍也是只能在屬性 中已定義的可選值中進行選擇。

對應的UI如下:

聚合SQL應用:

首先,從應用來考慮:當用戶到這個界面時,首先顯示的是左邊那個Project(項目)的列表。當用戶點擊其中某個項目時,系統開始 獲取它下面的PBS,並顯示在項目PBS頁簽下。這裡的PBS有很多個,如果使用原有的LazyLoad的模式的話,必然造成多次的遠程連接。所以 這裡需要把整個項目的PBS都一次性獲取到客戶端,使用的方案正是前面所講到的聚合SQL。

但是由於一開始只顯示一個簡單的列表給用戶選擇,這時,不需要對所有項目都加載全部的數據。所以,這裡的聚合SQL只是取 ProjectPBS和PBSPropertyValue的連接。相關代碼如下:

最外層接口:(由於業務需要,這裡調用的是該項目對應的PBSType的PBS列表)

1 public static PBSs GetListByPBSTypeId_With_Properties(Guid pbsTypeId)
2 {
3     //... 
4 }

數據層:

01 private void DataPortal_Fetch(GetListCriteria_With_Properties criteria)
02 {
03     this.MarkAsChild();
04
05     var pbsTypeId = criteria.PBSTypeId;
06     var sql = string.Format(SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES, pbsTypeId);
07
08     using (var db = Helper.CreateDb())
09     {
10         var table = db.QueryTable(sql);
11         var list = GetChild();
12         list.ReadFromTable(table, PBS.GetChild_With_Properties);
13         foreach (var pbs in list.OrderBy(pbs => pbs.OrderNo))
14         {
15             this.Add(pbs);
16         }
17     }
18 }

SQL格式定義:

01 private static readonly string SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES = string.Format(@"
02 select
03 {0},
04 {1},
05 {2}
06 from PBS pbs
07     left outer join PBSProperty p on pbs.Id = p.PBSId
08     left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId
09 where pbs.PBSTypeId = '{{0}}'
10 order by pbs.Id, p.Id"
11 , PBS.GetReadableColumnsSql()
12 , PBSProperty.GetReadableColumnsSql("p")
13 , PBSPropertyOptionalValue.GetReadableColumnsSql("v"));

在這裡就不再對具體的代碼進行解釋,想進一步了解的讀者請查看前面的文章,有所有格式的詳細解釋。

預加載的應用:

在實際應用中,發現上面使用的聚合SQL獲取的對象列表,其包含的數據量比較大。當用戶選擇某個項目時,如果等待一次性把所有的 數據都加載好,再顯示界面給用戶,會造成界面停滯,用戶體驗降低。所以我們在這裡使用這樣的策略:

先正常顯示PBS的列表,然後開始使用後台線程預加載所有PBS的屬性。當數據沒有加載好時,用戶選擇某個PBS,同樣使用原來的模式 ,遠程獲取該 PBS下的屬性列表。這裡的數據量很小,可以忽略。當預加載完成後,把獲取到的所有屬性和當前已經綁定到界面中的對象 進行合並。這樣,如果用戶再選擇其它的PBS,就不會再發起遠程連接了。

看上去,以上的策略好像比較復雜,實現的代碼肯定比較繁瑣。不過,由於前面幾篇中提到的API設計,大大減少了代碼量。代碼如下 :

當用戶點擊某個項目時,開始預加載它的屬性列表:

01 EventHandler projectPBSView_DataChanged = (sender, e) =>
02 {
03     var project = view.CurrentObject as Project;
04
05     if (project != null)
06     {
07         project.PBSPropertyValuesLoader.BeginLoading();
08     }
09 };
10 projectPBSView.DataChanged += projectPBSView_DataChanged;

上面使用的是《性能優化總結(四):預加載的設計》中所設計的API:

01 public partial class Project : GEntity<Project>, ICopySource
02 {
03     [NonSerialized, NotUndoable]
04     private ForeAsyncLoader _PBSPropertyValuesLoader;
05
06     /// <summary>
07     /// 屬性值的加載器,一次性加載項目下的所有屬性。
08     ///  
09     /// 加載以下數據:ProjectPBSs.ProjectPBSPropertyValues
10     /// </summary>
11     public ForeAsyncLoader PBSPropertyValuesLoader
12     {
13         get
14         {
15             if (this._PBSPropertyValuesLoader == null)
16             {
17                 this._PBSPropertyValuesLoader = new ForeAsyncLoader (this.LoadPBSPropertyValues);
18             }
19             return this._PBSPropertyValuesLoader;
20         }
21     }
22 }

數據未加載完成時,用戶選擇PBS,使用的依然是原有的LazyLoad屬性:

01 public class PBS : GTreeEntity<PBS>, IDisplayModel
02 {
03     private static PropertyInfo<PBSPropertys> PBSPropertysProperty =
04         RegisterProperty(new PropertyInfo<PBSPropertys>("PBSPropertys"));
05     [Association]
06     public PBSPropertys PBSPropertys
07     {
08         get
09         {
10             //LazyLoad
11             //如果屬性不存在時,會造成遠程獲取數據。
12             return this.GetLazyChildren(PBSPropertysProperty, PBSPropertys.NewChild,  PBSPropertys.Get);
13         }
14     }
15 }

數據加載完成,我們需要合並對象的數據。這裡需要一些額外的思考,請接著看:

新的問題:合並數據

當大量的對象數據到達客戶端後,由於我們沒有使用“唯一實體”的技術(可以簡單理解為:同一個ID的實體,內存中只有唯一一個對 象,不存在其它的拷貝。),所以需要把這些對象中的數據都合並到綁定到UI的對象中。我們接著上面的應用場景進行考慮:由於獲取時 間相對較長,所以在數據到達之前,用戶可能已經選擇了某些PBS並對其下的屬性進行了編輯。這時,如果我們進行簡單的拷貝,必然導致 數據丟失。

所以我們需要在加載每一個PBS的屬性時,先判定是否已經獲取過了。數據加載過程變成了這樣:

01 /// <summary>
02 /// 緩存是否加載的結果。
03 /// </summary>
04 [NonSerialized]
05 private bool _PBSPropertyValuesLoaded;
06
07 /// <summary>
08 /// 一次性加載所有PBS的屬性值。
09 /// </summary>
10 private void LoadPBSPropertyValues()
11 {
12     //如果所有屬性都加載好了,就不需要執行下面的過程了。
13     if (this._PBSPropertyValuesLoaded) return;
14
15     lock (this)
16     {
17         //計算是否已經全部加載好了所有的屬性。
18         var pbssLoaded = this.FieldManager.FieldExists(ProjectPBSsProperty);
19         ProjectPBS[] projectPBSCache = null;
20         if (pbssLoaded)
21         {
22             projectPBSCache = this.ProjectPBSs.ToArray();
23             this._PBSPropertyValuesLoaded = projectPBSCache.All(pp => pp.PropertyValuesLoaded ());
24             if (this._PBSPropertyValuesLoaded) return;
25         }
26
27         //開始加載 
28         var oldProjectPBSs = ProjectPBSs.GetListByProject_With_PropertyValues(this);
29
30         if (pbssLoaded)
31         {
32             foreach (var projectPBS in projectPBSCache)
33             {
34                 projectPBS.LoadPropertyValues(oldProjectPBSs);
35             }
36         }
37         else
38         {
39             this.LoadProperty(ProjectPBSsProperty, oldProjectPBSs);
40         }
41
42         //加載完成,緩存結果
43         this._PBSPropertyValuesLoaded = true;
44     }
45 }

其中Lock用於鎖住根對象,防止多個異步加載過程,同時對數據進行設置。

尾聲

GIX4系統在經歷了本次有針對性的優化後,提升了不少用戶體驗。實施人員的原話如下:“小胡,這次用戶覺得軟件快了好多。你們早 這樣做就好了嘛……”。

接下來我們要考慮的重點是,對現有設計進行重構。重點是如何能更簡單地使用聚合加載。現在要實現一個聚合加載,從編寫SQL,到 方法定義都比較繁瑣。一次加載可能需要寫好幾個方法。雖然每個方法也就幾行,但是定義起來確實麻煩……

再簡單下去……我可不想造輪子,再寫一個無聊的ORM!

本系列至此告一段落。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved