本文中的内容与前面几篇的内容、与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
);
|
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
);
|
总体设计
本次设计,主要是以提高模块的易用性为目的。
在原有的设计中,主要有两个步骤,生成聚合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的获取不再浪费,印证了:“一个重构后良好结构的程序,性能很有可能会有所提升。”