优化OEA中的聚合SQL

简介:

   本文中的内容与前面几篇的内容、与OEA框架中的内容相关性比较大,有兴趣的朋友可以关注CodePlex中的项目:《OpenExpressApp

 

结果对比


    优化前的代码,在前面的文章中已经有所展示。这里主要看一下优化过后的代码:

最简单的聚合SQL生成:

1
2
3
4
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,如:

1
2
3
4
var  pbsList = AggregateSQL.Instance.LoadEntities<PBS>(
     option => option.LoadChildren(pbs => pbs.PBSBQItems),
     pbsTypeId
     );
这样,API内部会生成聚合SQL,并进行聚合对象的加载。相对以前的模式,易用性提高了许多。这里,再给出一个目前支持的比较完整的API示例:
1
2
3
4
5
6
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。

image

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

 

核心数据结构 - 加载选项


 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <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 则表示一个加载项,它主要包含一个属性的元数据,用于表示要级联加载的子对象集合属性或者外键引用对象属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <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方法的,也不复杂,贴下代码,不再一一描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/// <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》中提到的设计原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/// <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的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”

目录
相关文章
|
25天前
|
SQL 存储 关系型数据库
一文搞懂SQL优化——如何高效添加数据
**SQL优化关键点:** 1. **批量插入**提高效率,一次性建议不超过500条。 2. **手动事务**减少开销,多条插入语句用一个事务。 3. **主键顺序插入**避免页分裂,提升性能。 4. **使用`LOAD DATA INFILE`**大批量导入快速。 5. **避免主键乱序**,减少不必要的磁盘操作。 6. **选择合适主键类型**,避免UUID或长主键导致的性能问题。 7. **避免主键修改**,保持索引稳定。 这些技巧能优化数据库操作,提升系统性能。
220 4
一文搞懂SQL优化——如何高效添加数据
|
1月前
|
SQL 存储 数据库连接
日活3kw下,如何应对实际业务场景中SQL过慢的优化挑战?
在面试中,SQL调优是一个常见的问题,通过这个问题可以考察应聘者对于提升SQL性能的理解和掌握程度。通常来说,SQL调优需要按照以下步骤展开。
|
2天前
|
SQL 分布式计算 资源调度
一文解析 ODPS SQL 任务优化方法原理
本文重点尝试从ODPS SQL的逻辑执行计划和Logview中的执行计划出发,分析日常数据研发过程中各种优化方法背后的原理,覆盖了部分调优方法的分析,从知道怎么优化,到为什么这样优化,以及还能怎样优化。
|
9天前
|
SQL 关系型数据库 数据库
【后端面经】【数据库与MySQL】SQL优化:如何发现SQL中的问题?
【4月更文挑战第12天】数据库优化涉及硬件升级、操作系统调整、服务器/引擎优化和SQL优化。SQL优化目标是减少磁盘IO和内存/CPU消耗。`EXPLAIN`命令用于检查SQL执行计划,关注`type`、`possible_keys`、`key`、`rows`和`filtered`字段。设计索引时考虑外键、频繁出现在`where`、`order by`和关联查询中的列,以及区分度高的列。大数据表改结构需谨慎,可能需要停机、低峰期变更或新建表。面试中应准备SQL优化案例,如覆盖索引、优化`order by`、`count`和索引提示。优化分页查询时避免大偏移量,可利用上一批的最大ID进行限制。
35 3
|
26天前
|
SQL 关系型数据库 MySQL
【MySQL技术之旅】(7)总结和盘点优化方案系列之常用SQL的优化
【MySQL技术之旅】(7)总结和盘点优化方案系列之常用SQL的优化
39 1
|
27天前
|
SQL 索引
SQL怎么优化
SQL怎么优化
30 2
|
1月前
|
SQL 监控 测试技术
SQL语法优化与最佳实践
【2月更文挑战第28天】本章将深入探讨SQL语法优化的重要性以及具体的优化策略和最佳实践。通过掌握和理解这些优化技巧,读者将能够编写出更高效、更稳定的SQL查询,提升数据库性能,降低系统资源消耗。
|
1月前
|
SQL 关系型数据库 MySQL
[MySQL]SQL优化之sql语句优化
[MySQL]SQL优化之sql语句优化
|
1月前
|
SQL 关系型数据库 MySQL
[MySQL]SQL优化之索引的使用规则
[MySQL]SQL优化之索引的使用规则
|
1月前
|
SQL 存储 关系型数据库
[MySQL] SQL优化之性能分析
[MySQL] SQL优化之性能分析