前面已經把原理都講了一遍,這篇主要是給出一個應用的實例。該實例取自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!
本系列至此告一段落。