之前寫過幾篇關於彙總對象SQL的文章,講的是如果設計架構,使用一句SQL語句來載入整個彙總對象樹中的所有資料。相關內容,參見:《效能最佳化總結(二):彙總SQL》、《效能最佳化總結(三):彙總SQL在GIX4中的應用》。由於沒有使用其它的ORM架構,當時項目組決定做彙總SQL,主要是為了減少SQL查詢的次數,來提升部分模組的效能。現在看來,當時雖然達到了這個目標,但是彙總SQL的API卻不簡單,使用極為不便。至今,項目組中的其它人也不會使用。所以,這次我們決定把彙總SQL的API使用再次進行封裝,以達到使用起來更簡便的效果。
本文中的內容與前面幾篇的內容、與OEA架構中的內容相關性比較大,有興趣的朋友可以關注CodePlex中的項目:《OpenExpressApp》
結果對比
最佳化前的代碼,在前面的文章中已經有所展示。這裡主要看一下最佳化過後的代碼:
最簡單的彙總SQL產生:
var sqlSimple = AggregateSQL.Instance.GenerateQuerySQL<PBS>( option => option.LoadChildren(pbs => pbs.PBSBQItems), pbsTypeId );
這樣就產生了如下SQL:
SELECT
pbs0.pid as PBS_pid, pbs0.pbstypeid as PBS_pbstypeid, pbs0.code as PBS_code, pbs0.name as PBS_name, pbs0.fullname as PBS_fullname, pbs0.description as PBS_description, pbs0.pbssubjectid as PBS_pbssubjectid, pbs0.orderno as PBS_orderno, pbs0.id as PBS_id,
pbsbqi1.pbsid as PBSBQItem_pbsid, pbsbqi1.code as PBSBQItem_code, pbsbqi1.name as PBSBQItem_name, pbsbqi1.unit as PBSBQItem_unit, pbsbqi1.bqdbid as PBSBQItem_bqdbid, pbsbqi1.id as PBSBQItem_id
FROM PBS AS pbs0
LEFT OUTER JOIN PBSBQItem AS pbsbqi1 ON pbsbqi1.PBSId = pbs0.Id
WHERE pbs0.PBSTypeId = '084a7db5-938a-4c7b-8d6a-612146ad87f9'
ORDER BY pbs0.Id, pbsbqi1.Id
該SQL用於載入彙總根對象PBSType下的所有PBS子物件,同時每個PBS的子物件PBSBQItems也都被同時查詢出來。
再進一步,我們還可以直接使用彙總關係載入出對象,而不需要SQL,如:
var pbsList = AggregateSQL.Instance.LoadEntities<PBS>( option => option.LoadChildren(pbs => pbs.PBSBQItems), pbsTypeId );
這樣,API內部會產生彙總SQL,並進行彙總對象的載入。相對以前的模式,易用性提高了許多。這裡,再給出一個目前支援的比較完整的API樣本:
var projectPBSs = AggregateSQL.Instance.LoadEntities<ProjectPBS>(loadOptions => loadOptions.LoadChildren(pp => pp.ProjectPBSPropertyValues) .Order<ProjectPBSPropertyValue>().By(v => v.PBSProperty.OrderNo) .LoadFK(v => v.PBSProperty).LoadChildren(p => p.PBSPropertyOptionalValues), criteria.ProjectId );
表示:載入ProjectPBS的對象列表時:同時載入它每一個ProjectPBS的子物件列表ProjectPBSPropertyValues,並把ProjectPBSPropertyValues按照外鍵PBSProperty的OrderNo屬性進行排序;同時,載入ProjectPBSPropertyValue.PBSProperty、載入PBSProperty.PBSPropertyOptionalValues。(其中,Order方法需要使用泛型方法指明類型是因為目前的實體列表都是非泛型的,不能進行類型推斷。)
總體設計
本次設計,主要是以提高模組的易用性為目的。
在原有的設計中,主要有兩個步驟,產生彙總SQL 和 從大表中載入彙總對象。這兩個過程是比較獨立的。它們之間耦合的地方有兩個。首先,是為表產生什麼樣的列名,產生SQL時按照這種列名的約定進行產生,載入對象時則在大表中找對應列的資料。其次,它們還隱含耦合一些說明性的資料,這些資料指明了需要載入哪些子屬性或者外鍵,什麼樣的載入關係,對應一個什麼樣的彙總SQL,也就對應載入出來的對象。
也就是說,上述兩個過程需要完整的封裝起來,我們需要管理好這兩個部分。而列名的產生在原來的模式中已經使用了“表名+列名”的格式進行了約定,所以現在我們只需要把“描述如何載入的描述性資料”進行管理就可以了。有了這些資料,則可以在架構內部產生彙總SQL,在架構內部按照它們進行大表到彙總對象的載入。以下,我將這些資料稱為彙總對象的“載入選項”。
同時,考慮到彙總SQL產生的複雜性及使用的2/8原則,這次的彙總SQL自動產生和載入只處理比較簡單的情況:只處理簡單的鏈式的載入。例如:A對象作為Root的子物件,它還有子物件B、C,B有子物件D、E,D有外鍵引用對象F、F有子物件G,那麼,只處理鏈式的載入意味著,最多可以在載入某個Root對象的A集合的同時,帶上A.B、B.C、C.D、D.F、F.G。
如所示,在載入A.B的時候,不支援載入A.C;同理,載入B.D的時候,不支援載入B.E。其實在實際運用當中,這樣的局限性在使用的時候並沒有太大的問題,一是較多的使用情境不需要同時載入所有的子,二是可以分兩條線載入對象後,再使用對象進行資料的融合。
核心資料結構 - 載入選項
上面已經說明了載入選項是整個彙總SQL載入的描述資料,描述如何產生SQL,描述如何載入對象。它其實也就是整個過程中的核心對象,由於時間有限(預計只有一天時間完成整個設計及代碼實現),而且這個對象並不會直接暴露在外面,所以這直接使用了最簡單的鏈表類型來表示鏈式的載入選項。(老實說,這個設計的擴充性並不好。)
/// <summary>/// 彙總載入描述器。/// /// 目前只包含一些彙總載入選項“AggregateSQLItem”/// </summary>internal class AggregateDescriptor{ private LinkedList<LoadOptionItem> _items = new LinkedList<LoadOptionItem>(); /// <summary> /// 所有的AggregateSQLItem /// </summary> internal LinkedList<LoadOptionItem> Items { get { return _items; } } /// <summary> /// 直接載入的實體類型 /// </summary> internal Type DirectlyQueryType { get { return this._items.First.Value.OwnerType; } } /// <summary> /// 追加一個彙總載入選項 /// </summary> /// <param name="item"></param> internal void AddItem(LoadOptionItem item) { this._items.AddLast(item); }}
而它包含的每一個元素 LoadOptionItem 則表示一個附加元件,它主要包含一個屬性的中繼資料,用於表示要級聯載入的子物件集合屬性或者外鍵引用對象屬性。
/// <summary>/// 產生彙總SQL的附加元件中的某一項/// </summary>[DebuggerDisplay("{OwnerType.Name}.{PropertyEntityType.Name}")]internal class LoadOptionItem{ private Action<Entity, Entity> _fkSetter; /// <summary> /// 載入這個屬性。 /// </summary> internal IPropertyInfo PropertyInfo { get; private set; } internal Func<Entity, object> OrderBy { get; set; } /// <summary> /// 指標這個屬性是一般的實體 /// </summary> internal AggregateLoadType LoadType { get { return this._fkSetter == null ? AggregateLoadType.Children : AggregateLoadType.ReferenceEntity; } } //.......}/// <summary>/// 屬性的載入類型/// </summary>internal enum AggregateLoadType{ /// <summary> /// 載入子物件集合屬性 /// </summary> Children, /// <summary> /// 載入外鍵引用實體。 /// </summary> ReferenceEntity}
對象載入
按照上面的載入選項的鏈式設計,SQL產生其實就比較簡單了:列名產生還是使用原有的方法,其它部分則只需要按照中繼資料進行鏈式產生就行了。花些時間就搞定了。
架構中對象的彙總載入的實現,和手寫時一樣,也是基於原有的ReadFromTable方法的,也不複雜,貼下代碼,不再一一描述:
/// <summary>/// 彙總實體的載入器/// </summary>internal class AggregateEntityLoader{ private AggregateDescriptor _aggregateInfo; internal AggregateEntityLoader(AggregateDescriptor aggregate) { if (aggregate == null) throw new ArgumentNullException("aggregate"); if (aggregate.Items.Count < 1) throw new InvalidOperationException("aggregate.Items.Count < 2 must be false."); this._aggregateInfo = aggregate; } /// <summary> /// 通過彙總SQL載入整個彙總對象列表。 /// </summary> /// <param name="sql"></param> /// <returns></returns> internal EntityList Query(string sql) { IGTable dataTable = null; IDbFactory dbFactory = this._aggregateInfo.Items.First.Value.OwnerRepository; using (var db = dbFactory.CreateDb()) { dataTable = db.QueryTable(sql); } //使用dataTable中的資料 和 AggregateDescriptor 中的描述資訊,讀取整個彙總列表。 var list = this.ReadFromTable(dataTable, this._aggregateInfo.Items.First); return list; } /// <summary> /// 根據 optionNode 中的描述資訊,讀取 table 中的資料群組裝為對象列表並返回。 /// /// 如果 optionNode 中指定要載入更多的子/引用對象,則會遞迴調用自己實現彙總載入。 /// </summary> /// <param name="table"></param> /// <param name="optionNode"></param> /// <returns></returns> private EntityList ReadFromTable(IGTable table, LinkedListNode<LoadOptionItem> optionNode) { var option = optionNode.Value; var newList = option.OwnerRepository.NewList(); newList.ReadFromTable(table, (row, subTable) => { var entity = option.OwnerRepository.Convert(row); EntityList listResult = null; //是否還有後繼需要載入的對象?如果是,則遞迴調用自己進行子物件的載入。 var nextNode = optionNode.Next; if (nextNode != null) { listResult = this.ReadFromTable(subTable, nextNode); } else { listResult = this.ReadFromTable(subTable, option.PropertyEntityRepository); } //是否需要排序? if (listResult.Count > 1 && option.OrderBy != null) { listResult = option.PropertyEntityRepository.NewListOrderBy(listResult, option.OrderBy); } //當前對象是載入類型的子物件還是引用的外鍵 if (option.LoadType == AggregateLoadType.Children) { listResult.SetParentEntity(entity); entity.LoadCSLAProperty(option.CslaPropertyInfo, listResult); } else { if (listResult.Count > 0) { option.SetReferenceEntity(entity, listResult[0]); } } return entity; }); return newList; } /// <summary> /// 簡單地從table中載入指定的實體列表。 /// </summary> /// <param name="table"></param> /// <param name="repository"></param> /// <returns></returns> private EntityList ReadFromTable(IGTable table, EntityRepository repository) { var newList = repository.NewList(); newList.ReadFromTable(table, (row, subTable) => repository.Convert(row)); return newList; }}
美化的API
基於以上的基礎,我們需要一個流暢的API來定義載入選項。這一點對於一個架構設計人員來說,往往很重要,只有流暢、易用的API才能對得起你的客戶:架構使用者。以下我只把給出幾個為達到流暢API而特別設計的類。其中,用到了《小技巧 - 簡化你的泛型API》中提到的設計原則。
/// <summary>/// 儲存了載入選項項/// </summary>public abstract class LoadOptionSelector{ internal LoadOptionSelector(AggregateDescriptor descriptor) { _descriptor = descriptor; } private AggregateDescriptor _descriptor; internal AggregateDescriptor InnerDescriptor { get { return _descriptor; } }}/// <summary>/// 屬性選取器/// </summary>/// <typeparam name="TEntity"></typeparam>public class PropertySelector<TEntity> : LoadOptionSelector where TEntity : Entity{ internal PropertySelector(AggregateDescriptor descriptor) : base(descriptor) { } /// <summary> /// 需要同時載入外鍵 /// </summary> /// <typeparam name="TFKEntity"></typeparam> /// <param name="fkEntityExp"> /// 需要載入的外鍵實體屬性運算式 /// </param> /// <returns></returns> public PropertySelector<TFKEntity> LoadFK<TFKEntity>(Expression<Func<TEntity, TFKEntity>> fkEntityExp) where TFKEntity : Entity { var entityPropertyName = GetPropertyName(fkEntityExp); var propertyName = entityPropertyName + "Id"; IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo(typeof(TEntity)); var propertyInfo = entityInfo.BOPropertyInfos.FirstOrDefault(p => p.Name == propertyName); //構造一個臨時代理方法,實現:TEntity.EntityProperty = TFKEntity var pE = System.Linq.Expressions.Expression.Parameter(typeof(TEntity), "e"); var pEFK = System.Linq.Expressions.Expression.Parameter(typeof(TFKEntity), "efk"); var propertyExp = System.Linq.Expressions.Expression.Property(pE, entityPropertyName); var body = System.Linq.Expressions.Expression.Assign(propertyExp, pEFK); var result = System.Linq.Expressions.Expression.Lambda<Action<TEntity, TFKEntity>>(body, pE, pEFK); var fkSetter = result.Compile(); var option = new LoadOptionItem(propertyInfo, (e, eFK) => fkSetter(e as TEntity, eFK as TFKEntity)); //避免迴圈 if (this.InnerDescriptor.Items.Any(i => i.OwnerType == option.PropertyEntityType)) { throw new InvalidOperationException("有迴圈的實體設定。"); } this.InnerDescriptor.AddItem(option); return new PropertySelector<TFKEntity>(this.InnerDescriptor); } /// <summary> /// 需要同時載入孩子 /// </summary> /// <typeparam name="TChildren"></typeparam> /// <param name="propExp"> /// 需要載入的孩子屬性運算式 /// </param> /// <returns></returns> public ChildrenSelector LoadChildren<TChildren>(Expression<Func<TEntity, TChildren>> propExp) where TChildren : EntityList { var propertyName = GetPropertyName(propExp); IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo(typeof(TEntity)); var propertyInfo = entityInfo.BOsPropertyInfos.FirstOrDefault(p => p.Name == propertyName); this.InnerDescriptor.AddItem(new LoadOptionItem(propertyInfo)); return new ChildrenSelector(this.InnerDescriptor); } private static string GetPropertyName<TProperty>(Expression<Func<TEntity, TProperty>> propExp) { var member = propExp.Body as MemberExpression; var property = member.Member as PropertyInfo; if (property == null) throw new ArgumentNullException("property"); var propertyName = property.Name; return propertyName; }}/// <summary>/// 孩子選取器/// </summary>/// <typeparam name="TEntity"></typeparam>public class ChildrenSelector : LoadOptionSelector{ internal ChildrenSelector(AggregateDescriptor descriptor) : base(descriptor) { } public OrderByLoadOption<TEntity> Order<TEntity>() where TEntity : Entity { return new OrderByLoadOption<TEntity>(this.InnerDescriptor); } /// <summary> /// 把孩子集合轉換為實體物件,需要繼續載入它的子物件 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> public PropertySelector<TEntity> Continue<TEntity>() where TEntity : Entity { return new PropertySelector<TEntity>(this.InnerDescriptor); }}public class OrderByLoadOption<TEntity> : LoadOptionSelector where TEntity : Entity{ internal OrderByLoadOption(AggregateDescriptor descriptor) : base(descriptor) { } public PropertySelector<TEntity> By<TKey>(Func<TEntity, TKey> keySelector) { this.InnerDescriptor.Items.Last.Value .OrderBy = e => keySelector(e as TEntity); return new PropertySelector<TEntity>(this.InnerDescriptor); }}
小結
本次重構由於只處理“鏈式的載入選項”,所以實現並不複雜。同時,由於把Repository都臨時存放在了LoadOptionItem中,使得Repository的擷取不再浪費,印證了:“一個重構後良好結構的程式,效能很有可能會有所提升。”