之前写过几篇关于聚合对象SQL的文章,讲的是如果设计框架,使用一句SQL语句来加载整个聚合对象树中的所有数据。相关内容,参见:《性能优化总结(二):聚合SQL》、《性能优化总结(三):聚合SQL在GIX4中的应用》。由于没有使用其它的ORM框架,当时项目组决定做聚合SQL,主要是为了减少SQL查询的次数,来提升部分模块的性能。现在看来,当时虽然达到了这个目标,但是聚合SQL的API却不简单,使用极为不便。至今,项目组中的其它人也不会使用。所以,这次我们决定把聚合SQL的API使用再次进行封装,以达到使用起来更简便的效果。

本文中的内容与前面几篇的内容、与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。

如上图所示,在加载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的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”

本文转自BloodyAngel博客园博客,原文链接:http://www.cnblogs.com/zgynhqf/archive/2011/01/07/1930137.html,如需转载请自行联系原作者

优化OEA中的聚合SQL相关推荐

  1. 性能优化总结(三):聚合SQL在GIX4中的应用

    本节主要介绍,在GIX4系统中,如何应用上篇讲的方案来改善性能,如果与现有的系统环境集成在一起.大致包含以下内容: SQL的生成 映射-数据读取方案 工厂方法-接口的命名约定 实例代码 SQL生成 G ...

  2. sql优化基数和耗费_基数估计在SQL Server优化过程中的位置

    sql优化基数和耗费 In this blog post, I'm going to look at the place of the Cardinality Estimation Process i ...

  3. day12_oracle hint——SQL优化过程中常见Oracle中HINT的30个用法

    在SQL语句优化过程中,经常会用到hint, 以下是在SQL优化过程中常见Oracle中"HINT"的30个用法 1. /*+ALL_ROWS*/ 表明对语句块选择基于开销的优化方 ...

  4. oracle delete not in 优化,Oracle中的sql语句优化

    1.选择最有效率的表名顺序(只在基于规则的优化器中有效) ORACLE的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表(基础表driving table)将被最先处理,在 ...

  5. SQL中的聚合函数介绍

    什么是聚合函数(aggregate function)? 聚合函数对一组值执行计算并返回单一的值. 聚合函数有什么特点? 除了 COUNT 以外,聚合函数忽略空值. 聚合函数经常与 SELECT 语句 ...

  6. mysql 如何优化sql语句,如何优化SQL?MySQL中超实用的SQL语句送给大家

    如何优化SQL?MySQL中超实用的SQL语句送给大家 如何优化SQL?MySQL中超实用的SQL语句送给大家 在写SQL时,经常灵活运用一些SQL语句编写的技巧,可以大大简化程序逻辑.减少程序与数据 ...

  7. ABAP 7.53 中的ABAP SQL(原Open SQL)新特性

    S/4 HANA 1809 已经在上月发布,随之而来的是ABAP 7.53. 本文是更新文档中ABAP SQL的部分的翻译. 本次更新的内容较多,主要内容包括:Open SQL更名为ABAP SQL: ...

  8. OEA中的缓存模块设计

    一般缓存介绍 网上介绍缓存的文章比较多,在这里我就挑点重点说一下. 缓存是信息系统软件硬件设计中常用的设计方法:从底层硬件的CPU结构中的多级缓存,到软件中操作系统中内存管理的设计,再到应用软件中的高 ...

  9. MySQL 性能优化:8 种常见 SQL 错误用法!

    声明:转载自 MySQL 性能优化:8 种常见 SQL 错误用法! 1.LIMIT 语句 分页查询是最常用的场景之一,但也通常也是最容易出问题的地方.比如对于下面简单的语句,一般 DBA 想到的办法是 ...

最新文章

  1. 中国剩余定理(Chinese Remainder Theorem)
  2. DefWindowProc
  3. 【机器学习基础】Python实现行转列?!超简单,赶快get起来
  4. Thymeleaf——使用模板动态生成JavaScript脚本文件
  5. 统计输入中数字出现的次数java,java实现统计文章(英文)中字母、数字、空格和其他字符出现的次数...
  6. C#中如何复制窗体到另一个项目
  7. eclipse 跑maven命令_eclipse中运行maven命令没有反应,console也不出现信息
  8. 用visio制作机柜服务器,ibm visio 服务器机柜图标
  9. 学校与工作(献于在校大学生及入职不久的工作者)
  10. 【机器学习】广义回归神经网络(GRNN)的python实现
  11. JavaScript执行机制-node事件循环
  12. (转载)python re模块详解 正则表达式
  13. 华为正式发布鸿蒙OS操作系统,分布式架构首次用于终端
  14. 本人从事软件技术开发也有多年,打算先尝试往外迈一步试试!
  15. 中英文说明书丨CalBioreagents ACTH抗原抗体对
  16. c语言字符串子串问题,C语言计算字符串子串出现的次数
  17. 一加7充电_一加7T充电、续航能力测评
  18. “线上围观”创先河 百合佳缘集团移动端上线多屏直播
  19. 关于PISO求解的一些理解
  20. 三年前下载量达600W的老游戏,没想到还能发光发热!

热门文章

  1. 数字图像处理第十章——图像分割
  2. 电流镜,运放,比较器等管子的尺寸选取,静态分析,Vdsat和mismatch的关系
  3. 你电脑的CPU里被藏了台“小电脑”,可怕的是你什么都做不了
  4. 【项目】健康项目day4总结
  5. css控制背景图片随div的大小而缩放
  6. javascript英语单词音节拆分_英语单词音节的划分:成音节详解
  7. Kerberos 学习笔记
  8. ADS1118的MSP430驱动代码,非常方便移植
  9. Redis基础及原理详解
  10. 努力与优秀的人在人民大学与女王金融硕士中相遇