优化OEA中的聚合SQL
之前写过几篇关于聚合对象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的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”