[转载]优化OEA中的聚合SQL – 哲学驱动设计 – 博客园.
之前写过几篇关于聚合对象SQL的文章,讲的是如果设计框架,使用一句SQL语句来加载整个聚合对象树中的所有数据。相关内容,参见:《性能优化总结(二):聚合SQL》、《性能优化总结(三):聚合SQL在GIX4中的应用》。 由于没有使用其它的ORM框架,当时项目组决定做聚合SQL,主要是为了减少SQL查询的次数,来提升部分模块的性能。现在看来,当时虽然达到了这个目 标,但是聚合SQL的API却不简单,使用极为不便。至今,项目组中的其它人也不会使用。所以,这次我们决定把聚合SQL的API使用再次进行封装,以达 到使用起来更简便的效果。
本文中的内容与前面几篇的内容、与OEA框架中的内容相关性比较大,有兴趣的朋友可以关注CodePlex中的项目:《OpenExpressApp》
结果对比
优化前的代码,在前面的文章中已经有所展示。这里主要看一下优化过后的代码:
最简单的聚合SQL生成:
1 |
var sqlSimple = AggregateSQL.Instance.GenerateQuerySQL<PBS>( |
2 |
option => option.LoadChildren(pbs => pbs.PBSBQItems), |
这样就生成了如下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,如:
1 |
var pbsList = AggregateSQL.Instance.LoadEntities<PBS>( |
2 |
option => option.LoadChildren(pbs => pbs.PBSBQItems), |
这样,API内部会生成聚合SQL,并进行聚合对象的加载。相对以前的模式,易用性提高了许多。这里,再给出一个目前支持的比较完整的API示例:
1 |
var projectPBSs = AggregateSQL.Instance.LoadEntities<ProjectPBS>(loadOptions => |
2 |
loadOptions.LoadChildren(pp => pp.ProjectPBSPropertyValues) |
3 |
.Order<ProjectPBSPropertyValue>().By(v => v.PBSProperty.OrderNo) |
4 |
.LoadFK(v => v.PBSProperty).LoadChildren(p => p.PBSPropertyOptionalValues), |
表 示:加载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,描述如何加载对象。它其实也就是整个过程中的核心对象,由于时间有限(预计 只有一天时间完成整个设计及代码实现),而且这个对象并不会直接暴露在外面,所以这直接使用了最简单的链表类型来表示链式的加载选项。(老实说,这个设计 的扩展性并不好。)
04 |
/// 目前只包含一些聚合加载选项“AggregateSQLItem” |
06 |
internal class AggregateDescriptor |
08 |
private LinkedList<LoadOptionItem> _items = new LinkedList<LoadOptionItem>(); |
11 |
/// 所有的AggregateSQLItem |
13 |
internal LinkedList<LoadOptionItem> Items |
24 |
internal Type DirectlyQueryType |
28 |
return this ._items.First.Value.OwnerType; |
35 |
/// <param name="item"></param> |
36 |
internal void AddItem(LoadOptionItem item) |
38 |
this ._items.AddLast(item); |
而它包含的每一个元素 LoadOptionItem 则表示一个加载项,它主要包含一个属性的元数据,用于表示要级联加载的子对象集合属性或者外键引用对象属性。
04 |
[DebuggerDisplay( "{OwnerType.Name}.{PropertyEntityType.Name}" )] |
05 |
internal class LoadOptionItem |
07 |
private Action<Entity, Entity> _fkSetter; |
12 |
internal IPropertyInfo PropertyInfo { get ; private set ; } |
14 |
internal Func<Entity, object > OrderBy { get ; set ; } |
19 |
internal AggregateLoadType LoadType |
23 |
return this ._fkSetter == null ? AggregateLoadType.Children : AggregateLoadType.ReferenceEntity; |
33 |
internal enum AggregateLoadType |
对象加载
按照上面的加载选项的链式设计,SQL生成其实就比较简单了:列名生成还是使用原有的方法,其它部分则只需要按照元数据进行链式生成就行了。花些时间就搞定了。
框架中对象的聚合加载的实现,和手写时一样,也是基于原有的ReadFromTable方法的,也不复杂,贴下代码,不再一一描述:
004 |
internal class AggregateEntityLoader |
006 |
private AggregateDescriptor _aggregateInfo; |
008 |
internal AggregateEntityLoader(AggregateDescriptor aggregate) |
010 |
if (aggregate == null ) throw new ArgumentNullException( "aggregate" ); |
011 |
if (aggregate.Items.Count < 1) throw new InvalidOperationException( "aggregate.Items.Count < 2 must be false." ); |
013 |
this ._aggregateInfo = aggregate; |
017 |
/// 通过聚合SQL加载整个聚合对象列表。 |
019 |
/// <param name="sql"></param> |
020 |
/// <returns></returns> |
021 |
internal EntityList Query( string sql) |
023 |
IGTable dataTable = null ; |
025 |
IDbFactory dbFactory = this ._aggregateInfo.Items.First.Value.OwnerRepository; |
026 |
using (var db = dbFactory.CreateDb()) |
028 |
dataTable = db.QueryTable(sql); |
032 |
var list = this .ReadFromTable(dataTable, this ._aggregateInfo.Items.First); |
038 |
/// 根据 optionNode 中的描述信息,读取 table 中的数据组装为对象列表并返回。 |
040 |
/// 如果 optionNode 中指定要加载更多的子/引用对象,则会递归调用自己实现聚合加载。 |
042 |
/// <param name="table"></param> |
043 |
/// <param name="optionNode"></param> |
044 |
/// <returns></returns> |
045 |
private EntityList ReadFromTable(IGTable table, LinkedListNode<LoadOptionItem> optionNode) |
047 |
var option = optionNode.Value; |
048 |
var newList = option.OwnerRepository.NewList(); |
049 |
newList.ReadFromTable(table, (row, subTable) => |
051 |
var entity = option.OwnerRepository.Convert(row); |
053 |
EntityList listResult = null ; |
056 |
var nextNode = optionNode.Next; |
057 |
if (nextNode != null ) |
059 |
listResult = this .ReadFromTable(subTable, nextNode); |
063 |
listResult = this .ReadFromTable(subTable, option.PropertyEntityRepository); |
067 |
if (listResult.Count > 1 && option.OrderBy != null ) |
069 |
listResult = option.PropertyEntityRepository.NewListOrderBy(listResult, option.OrderBy); |
073 |
if (option.LoadType == AggregateLoadType.Children) |
075 |
listResult.SetParentEntity(entity); |
076 |
entity.LoadCSLAProperty(option.CslaPropertyInfo, listResult); |
080 |
if (listResult.Count > 0) |
082 |
option.SetReferenceEntity(entity, listResult[0]); |
093 |
/// 简单地从table中加载指定的实体列表。 |
095 |
/// <param name="table"></param> |
096 |
/// <param name="repository"></param> |
097 |
/// <returns></returns> |
098 |
private EntityList ReadFromTable(IGTable table, EntityRepository repository) |
100 |
var newList = repository.NewList(); |
102 |
newList.ReadFromTable(table, (row, subTable) => repository.Convert(row)); |
美化的API
基于以上的基础,我们需要一个流畅的API来定义加载选项。这一点对于一个框架设计人员来说,往往很重要,只有流畅、易用的API才能对得起你的客户:框架使用者。以下我只把给出几个为达到流畅API而特别设计的类。其中,用到了《小技巧 – 简化你的泛型API》中提到的设计原则。
004 |
public abstract class LoadOptionSelector |
006 |
internal LoadOptionSelector(AggregateDescriptor descriptor) |
008 |
_descriptor = descriptor; |
011 |
private AggregateDescriptor _descriptor; |
013 |
internal AggregateDescriptor InnerDescriptor |
025 |
/// <typeparam name="TEntity"></typeparam> |
026 |
public class PropertySelector<TEntity> : LoadOptionSelector |
027 |
where TEntity : Entity |
029 |
internal PropertySelector(AggregateDescriptor descriptor) : base (descriptor) { } |
034 |
/// <typeparam name="TFKEntity"></typeparam> |
035 |
/// <param name="fkEntityExp"> |
038 |
/// <returns></returns> |
039 |
public PropertySelector<TFKEntity> LoadFK<TFKEntity>(Expression<Func<TEntity, TFKEntity>> fkEntityExp) |
040 |
where TFKEntity : Entity |
042 |
var entityPropertyName = GetPropertyName(fkEntityExp); |
043 |
var propertyName = entityPropertyName + "Id" ; |
045 |
IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo( typeof (TEntity)); |
046 |
var propertyInfo = entityInfo.BOPropertyInfos.FirstOrDefault(p => p.Name == propertyName); |
049 |
var pE = System.Linq.Expressions.Expression.Parameter( typeof (TEntity), "e" ); |
050 |
var pEFK = System.Linq.Expressions.Expression.Parameter( typeof (TFKEntity), "efk" ); |
051 |
var propertyExp = System.Linq.Expressions.Expression.Property(pE, entityPropertyName); |
052 |
var body = System.Linq.Expressions.Expression.Assign(propertyExp, pEFK); |
053 |
var result = System.Linq.Expressions.Expression.Lambda<Action<TEntity, TFKEntity>>(body, pE, pEFK); |
054 |
var fkSetter = result.Compile(); |
056 |
var option = new LoadOptionItem(propertyInfo, (e, eFK) => fkSetter(e as TEntity, eFK as TFKEntity)); |
059 |
if ( this .InnerDescriptor.Items.Any(i => i.OwnerType == option.PropertyEntityType)) |
061 |
throw new InvalidOperationException( "有循环的实体设置。" ); |
064 |
this .InnerDescriptor.AddItem(option); |
066 |
return new PropertySelector<TFKEntity>( this .InnerDescriptor); |
072 |
/// <typeparam name="TChildren"></typeparam> |
073 |
/// <param name="propExp"> |
076 |
/// <returns></returns> |
077 |
public ChildrenSelector LoadChildren<TChildren>(Expression<Func<TEntity, TChildren>> propExp) |
078 |
where TChildren : EntityList |
080 |
var propertyName = GetPropertyName(propExp); |
081 |
IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo( typeof (TEntity)); |
082 |
var propertyInfo = entityInfo.BOsPropertyInfos.FirstOrDefault(p => p.Name == propertyName); |
084 |
this .InnerDescriptor.AddItem( new LoadOptionItem(propertyInfo)); |
086 |
return new ChildrenSelector( this .InnerDescriptor); |
089 |
private static string GetPropertyName<TProperty>(Expression<Func<TEntity, TProperty>> propExp) |
091 |
var member = propExp.Body as MemberExpression; |
092 |
var property = member.Member as PropertyInfo; |
093 |
if (property == null ) throw new ArgumentNullException( "property" ); |
094 |
var propertyName = property.Name; |
103 |
/// <typeparam name="TEntity"></typeparam> |
104 |
public class ChildrenSelector : LoadOptionSelector |
106 |
internal ChildrenSelector(AggregateDescriptor descriptor) : base (descriptor) { } |
108 |
public OrderByLoadOption<TEntity> Order<TEntity>() |
109 |
where TEntity : Entity |
111 |
return new OrderByLoadOption<TEntity>( this .InnerDescriptor); |
115 |
/// 把孩子集合转换为实体对象,需要继续加载它的子对象 |
117 |
/// <typeparam name="TEntity"></typeparam> |
118 |
/// <returns></returns> |
119 |
public PropertySelector<TEntity> Continue<TEntity>() |
120 |
where TEntity : Entity |
122 |
return new PropertySelector<TEntity>( this .InnerDescriptor); |
126 |
public class OrderByLoadOption<TEntity> : LoadOptionSelector |
127 |
where TEntity : Entity |
129 |
internal OrderByLoadOption(AggregateDescriptor descriptor) : base (descriptor) { } |
131 |
public PropertySelector<TEntity> By<TKey>(Func<TEntity, TKey> keySelector) |
133 |
this .InnerDescriptor.Items.Last.Value |
134 |
.OrderBy = e => keySelector(e as TEntity); |
136 |
return new PropertySelector<TEntity>( this .InnerDescriptor); |
小结
本次重构由于只处理“链式的加载选项”,所以实现并不复杂。同时,由于把Repository都临时存放在了LoadOptionItem中,使得Repository的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”