[转载]优化OEA中的聚合SQL

[转载]优化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),
3 pbsTypeId
4 );

这样就生成了如下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),
3 pbsTypeId
4 );

这样,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),
5 criteria.ProjectId
6 );

表 示:加载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。

image

如上图所示,在加载A.B的时候,不支持加载A.C;同理,加载B.D的时候,不支持加载B.E。其实在实际运用当中,这样的局限性在使用的时候并没有太大的问题,一是较多的使用场景不需要同时加载所有的子,二是可以分两条线加载对象后,再使用对象进行数据的融合。

核心数据结构 – 加载选项


上面已经说明了加载选项是整个聚合SQL加载的描述数据,描述如何生成SQL,描述如何加载对象。它其实也就是整个过程中的核心对象,由于时间有限(预计 只有一天时间完成整个设计及代码实现),而且这个对象并不会直接暴露在外面,所以这直接使用了最简单的链表类型来表示链式的加载选项。(老实说,这个设计 的扩展性并不好。)

01 /// <summary>
02 /// 聚合加载描述器。
03 ///
04 /// 目前只包含一些聚合加载选项“AggregateSQLItem”
05 /// </summary>
06 internal class AggregateDescriptor
07 {
08 private LinkedList<LoadOptionItem> _items = new LinkedList<LoadOptionItem>();
09
10 /// <summary>
11 /// 所有的AggregateSQLItem
12 /// </summary>
13 internal LinkedList<LoadOptionItem> Items
14 {
15 get
16 {
17 return _items;
18 }
19 }
20
21 /// <summary>
22 /// 直接加载的实体类型
23 /// </summary>
24 internal Type DirectlyQueryType
25 {
26 get
27 {
28 return this._items.First.Value.OwnerType;
29 }
30 }
31
32 /// <summary>
33 /// 追加一个聚合加载选项
34 /// </summary>
35 /// <param name="item"></param>
36 internal void AddItem(LoadOptionItem item)
37 {
38 this._items.AddLast(item);
39 }
40 }

而它包含的每一个元素 LoadOptionItem 则表示一个加载项,它主要包含一个属性的元数据,用于表示要级联加载的子对象集合属性或者外键引用对象属性。

01 /// <summary>
02 /// 生成聚合SQL的加载项中的某一项
03 /// </summary>
04 [DebuggerDisplay("{OwnerType.Name}.{PropertyEntityType.Name}")]
05 internal class LoadOptionItem
06 {
07 private Action<Entity, Entity> _fkSetter;
08
09 /// <summary>
10 /// 加载这个属性。
11 /// </summary>
12 internal IPropertyInfo PropertyInfo { get; private set; }
13
14 internal Func<Entity, object> OrderBy { get; set; }
15
16 /// <summary>
17 /// 指标这个属性是一般的实体
18 /// </summary>
19 internal AggregateLoadType LoadType
20 {
21 get
22 {
23 return this._fkSetter == null ? AggregateLoadType.Children : AggregateLoadType.ReferenceEntity;
24 }
25 }
26
27 //.......
28 }
29
30 /// <summary>
31 /// 属性的加载类型
32 /// </summary>
33 internal enum AggregateLoadType
34 {
35 /// <summary>
36 /// 加载子对象集合属性
37 /// </summary>
38 Children,
39
40 /// <summary>
41 /// 加载外键引用实体。
42 /// </summary>
43 ReferenceEntity
44 }

对象加载


按照上面的加载选项的链式设计,SQL生成其实就比较简单了:列名生成还是使用原有的方法,其它部分则只需要按照元数据进行链式生成就行了。花些时间就搞定了。

框架中对象的聚合加载的实现,和手写时一样,也是基于原有的ReadFromTable方法的,也不复杂,贴下代码,不再一一描述:

001 /// <summary>
002 /// 聚合实体的加载器
003 /// </summary>
004 internal class AggregateEntityLoader
005 {
006 private AggregateDescriptor _aggregateInfo;
007
008 internal AggregateEntityLoader(AggregateDescriptor aggregate)
009 {
010 if (aggregate == null) throw new ArgumentNullException("aggregate");
011 if (aggregate.Items.Count < 1) throw new InvalidOperationException("aggregate.Items.Count < 2 must be false.");
012
013 this._aggregateInfo = aggregate;
014 }
015
016 /// <summary>
017 /// 通过聚合SQL加载整个聚合对象列表。
018 /// </summary>
019 /// <param name="sql"></param>
020 /// <returns></returns>
021 internal EntityList Query(string sql)
022 {
023 IGTable dataTable = null;
024
025 IDbFactory dbFactory = this._aggregateInfo.Items.First.Value.OwnerRepository;
026 using (var db = dbFactory.CreateDb())
027 {
028 dataTable = db.QueryTable(sql);
029 }
030
031 //使用dataTable中的数据 和 AggregateDescriptor 中的描述信息,读取整个聚合列表。
032 var list = this.ReadFromTable(dataTable, this._aggregateInfo.Items.First);
033
034 return list;
035 }
036
037 /// <summary>
038 /// 根据 optionNode 中的描述信息,读取 table 中的数据组装为对象列表并返回。
039 ///
040 /// 如果 optionNode 中指定要加载更多的子/引用对象,则会递归调用自己实现聚合加载。
041 /// </summary>
042 /// <param name="table"></param>
043 /// <param name="optionNode"></param>
044 /// <returns></returns>
045 private EntityList ReadFromTable(IGTable table, LinkedListNode<LoadOptionItem> optionNode)
046 {
047 var option = optionNode.Value;
048 var newList = option.OwnerRepository.NewList();
049 newList.ReadFromTable(table, (row, subTable) =>
050 {
051 var entity = option.OwnerRepository.Convert(row);
052
053 EntityList listResult = null;
054
055 //是否还有后继需要加载的对象?如果是,则递归调用自己进行子对象的加载。
056 var nextNode = optionNode.Next;
057 if (nextNode != null)
058 {
059 listResult = this.ReadFromTable(subTable, nextNode);
060 }
061 else
062 {
063 listResult = this.ReadFromTable(subTable, option.PropertyEntityRepository);
064 }
065
066 //是否需要排序?
067 if (listResult.Count > 1 && option.OrderBy != null)
068 {
069 listResult = option.PropertyEntityRepository.NewListOrderBy(listResult, option.OrderBy);
070 }
071
072 //当前对象是加载类型的子对象还是引用的外键
073 if (option.LoadType == AggregateLoadType.Children)
074 {
075 listResult.SetParentEntity(entity);
076 entity.LoadCSLAProperty(option.CslaPropertyInfo, listResult);
077 }
078 else
079 {
080 if (listResult.Count > 0)
081 {
082 option.SetReferenceEntity(entity, listResult[0]);
083 }
084 }
085
086 return entity;
087 });
088
089 return newList;
090 }
091
092 /// <summary>
093 /// 简单地从table中加载指定的实体列表。
094 /// </summary>
095 /// <param name="table"></param>
096 /// <param name="repository"></param>
097 /// <returns></returns>
098 private EntityList ReadFromTable(IGTable table, EntityRepository repository)
099 {
100 var newList = repository.NewList();
101
102 newList.ReadFromTable(table, (row, subTable) => repository.Convert(row));
103
104 return newList;
105 }
106 }

美化的API


基于以上的基础,我们需要一个流畅的API来定义加载选项。这一点对于一个框架设计人员来说,往往很重要,只有流畅、易用的API才能对得起你的客户:框架使用者。以下我只把给出几个为达到流畅API而特别设计的类。其中,用到了《小技巧 – 简化你的泛型API》中提到的设计原则。

001 /// <summary>
002 /// 存储了加载选项项
003 /// </summary>
004 public abstract class LoadOptionSelector
005 {
006 internal LoadOptionSelector(AggregateDescriptor descriptor)
007 {
008 _descriptor = descriptor;
009 }
010
011 private AggregateDescriptor _descriptor;
012
013 internal AggregateDescriptor InnerDescriptor
014 {
015 get
016 {
017 return _descriptor;
018 }
019 }
020 }
021
022 /// <summary>
023 /// 属性选择器
024 /// </summary>
025 /// <typeparam name="TEntity"></typeparam>
026 public class PropertySelector<TEntity> : LoadOptionSelector
027 where TEntity : Entity
028 {
029 internal PropertySelector(AggregateDescriptor descriptor) : base(descriptor) { }
030
031 /// <summary>
032 /// 需要同时加载外键
033 /// </summary>
034 /// <typeparam name="TFKEntity"></typeparam>
035 /// <param name="fkEntityExp">
036 /// 需要加载的外键实体属性表达式
037 /// </param>
038 /// <returns></returns>
039 public PropertySelector<TFKEntity> LoadFK<TFKEntity>(Expression<Func<TEntity, TFKEntity>> fkEntityExp)
040 where TFKEntity : Entity
041 {
042 var entityPropertyName = GetPropertyName(fkEntityExp);
043 var propertyName = entityPropertyName + "Id";
044
045 IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo(typeof(TEntity));
046 var propertyInfo = entityInfo.BOPropertyInfos.FirstOrDefault(p => p.Name == propertyName);
047
048 //构造一个临时代理方法,实现:TEntity.EntityProperty = TFKEntity
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();
055
056 var option = new LoadOptionItem(propertyInfo, (e, eFK) => fkSetter(e as TEntity, eFK as TFKEntity));
057
058 //避免循环
059 if (this.InnerDescriptor.Items.Any(i => i.OwnerType == option.PropertyEntityType))
060 {
061 throw new InvalidOperationException("有循环的实体设置。");
062 }
063
064 this.InnerDescriptor.AddItem(option);
065
066 return new PropertySelector<TFKEntity>(this.InnerDescriptor);
067 }
068
069 /// <summary>
070 /// 需要同时加载孩子
071 /// </summary>
072 /// <typeparam name="TChildren"></typeparam>
073 /// <param name="propExp">
074 /// 需要加载的孩子属性表达式
075 /// </param>
076 /// <returns></returns>
077 public ChildrenSelector LoadChildren<TChildren>(Expression<Func<TEntity, TChildren>> propExp)
078 where TChildren : EntityList
079 {
080 var propertyName = GetPropertyName(propExp);
081 IEntityInfo entityInfo = ApplicationModel.GetBusinessObjectInfo(typeof(TEntity));
082 var propertyInfo = entityInfo.BOsPropertyInfos.FirstOrDefault(p => p.Name == propertyName);
083
084 this.InnerDescriptor.AddItem(new LoadOptionItem(propertyInfo));
085
086 return new ChildrenSelector(this.InnerDescriptor);
087 }
088
089 private static string GetPropertyName<TProperty>(Expression<Func<TEntity, TProperty>> propExp)
090 {
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;
095
096 return propertyName;
097 }
098 }
099
100 /// <summary>
101 /// 孩子选择器
102 /// </summary>
103 /// <typeparam name="TEntity"></typeparam>
104 public class ChildrenSelector : LoadOptionSelector
105 {
106 internal ChildrenSelector(AggregateDescriptor descriptor) : base(descriptor) { }
107
108 public OrderByLoadOption<TEntity> Order<TEntity>()
109 where TEntity : Entity
110 {
111 return new OrderByLoadOption<TEntity>(this.InnerDescriptor);
112 }
113
114 /// <summary>
115 /// 把孩子集合转换为实体对象,需要继续加载它的子对象
116 /// </summary>
117 /// <typeparam name="TEntity"></typeparam>
118 /// <returns></returns>
119 public PropertySelector<TEntity> Continue<TEntity>()
120 where TEntity : Entity
121 {
122 return new PropertySelector<TEntity>(this.InnerDescriptor);
123 }
124 }
125
126 public class OrderByLoadOption<TEntity> : LoadOptionSelector
127 where TEntity : Entity
128 {
129 internal OrderByLoadOption(AggregateDescriptor descriptor) : base(descriptor) { }
130
131 public PropertySelector<TEntity> By<TKey>(Func<TEntity, TKey> keySelector)
132 {
133 this.InnerDescriptor.Items.Last.Value
134 .OrderBy = e => keySelector(e as TEntity);
135
136 return new PropertySelector<TEntity>(this.InnerDescriptor);
137 }
138 }

小结


本次重构由于只处理“链式的加载选项”,所以实现并不复杂。同时,由于把Repository都临时存放在了LoadOptionItem中,使得Repository的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏