性能优化总结(三):聚合SQL在GIX4中的应用
本节主要介绍,在GIX4系统中,如何应用上篇讲的方案来改善性能,如果与现有的系统环境集成在一起。大致包含以下内容:
SQL的生成
映射-数据读取方案
工厂方法-接口的命名约定
实例代码
SQL生成
GIX4系统中的所有领域模型及分布式访问机制,使用CSLA作为底层框架。而ORM机制,使用了一个非常轻量级的开源代码LiteORM实现。模型类的定义,采用以下的格式:
可以看到,在类的元数据定义中(这里目前使用的是Attribute的形式),已经包含了对应数据表和列的信息。所以为SQL的自动化自成提供了一定的支持。
其实,由于目前对性能要求比较高的模块少,所以用于优化查询的SQL主要还是依靠人工手写。但是由于LiteORM框架的功能比较有限,所以这里查询出来的表格数据需要由我们自己来进行读取并封装对象。考虑到:1. 多表连接时,列名可能会重复;2. 添加/删除列时,不要更改手写的SQL。所以至少列名应该自动生成,并不重复。我们把生成列名SQL的API都放在了所有模型的基类GEntity<T>中,如下:
[Serializable] public abstract class GEntity<T> : Entity<T> where T : GEntity<T> { #region 直接与数据行进行交互
/// <summary> /// 直接从数据集中取数据。 /// /// 注意: /// 数据集中的列字段约定为:“表名_列名”,如“PBS_Name”。 /// 默认使用反射创建对象并读取数据!同“LiteORM”。 /// /// 意义: /// 由于各个类的列名不再相同,所以这个方法的意义在于可以使用一句复杂的组合SQL加载一个聚合对象! /// </summary> /// <param name="rowData"> /// 这个数据集中的列字段约定为:“表名_列名”,如“PBS_Name”。 /// </param> /// <returns> /// 如果id值为null,则返回null。 /// </returns> public static T ReadDataDirectly(DataRow rowData) /// <summary> /// 获取可用于ReadDirectly方法读取的列名表示法。如: /// PBS.Id as PBS_Id, PBS.Name as PBS_Name, ........ /// </summary> /// <returns></returns> public static string GetReadableColumnsSql() /// <summary> /// 获取可用于ReadDirectly方法读取的列名表示法。如: /// p.Id as PBS_Id, p.Name as PBS_Name, ........ /// </summary> /// <param name="tableAlias">表í的?别e名?</param> /// <returns></returns> public static string GetReadableColumnsSql(string tableAlias) /// <summary> /// 获取columnName在DataRow中使用时约定的列名。 /// </summary> /// <param name="columnName"></param> /// <returns></returns> public static string GetReadableColumnSql(string columnName) public static ITable GetTableInfo() { //这里加载表信息时,可能需要和服务器交互。 if (Helper.IsOnServer()) { return GetTableInfo_OnServer(); } else { return GetTableInfo_OnClient(); } }
#endregion
例如,一个比较简单的聚合SQL如下:
private static readonly string SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES = string.Format(@" select {0}, {1}, {2} from PBS pbs left outer join PBSProperty p on pbs.Id = p.PBSId left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId where pbs.PBSTypeId = '{{0}}' order by pbs.Id, p.Id" , PBS.GetReadableColumnsSql() , PBSProperty.GetReadableColumnsSql("p") , PBSPropertyOptionalValue.GetReadableColumnsSql("v"));
这个SQL格式生成的结果存储在静态字段中,不需要每次都生成。最后生成的SQL语句如下:
select
pbs.pid as pbs_pid, pbs.pbstypeid as pbs_pbstypeid, pbs.code as pbs_code, pbs.name as pbs_name, pbs.description as pbs_description, pbs.orderno as pbs_orderno, pbs.id as pbs_id,
p.pbsid as pbsproperty_pbsid, p.code as pbsproperty_code, p.orderno as pbsproperty_orderno, p.pid as pbsproperty_pid, p.name as pbsproperty_name, p.unit as pbsproperty_unit, p.valuetype as pbsproperty_valuetype, p.editortype as pbsproperty_editortype, p.calcitemtype as pbsproperty_calcitemtype, p.isusedtoquery as pbsproperty_isusedtoquery, p.isrequired as pbsproperty_isrequired, p.isusedtosummarize as pbsproperty_isusedtosummarize, p.id as pbsproperty_id,
v.pbspropertyid as pbspropertyoptionalvalue_pbspropertyid, v.value as pbspropertyoptionalvalue_value, v.description as pbspropertyoptionalvalue_description, v.id as pbspropertyoptionalvalue_id
from PBS pbs
left outer join PBSProperty p on pbs.Id = p.PBSId
left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId
where pbs.PBSTypeId = '{0}'
order by pbs.Id, p.Id
映射-数据读取方案
SQL已经生成了,接下来就是把整个一张大表读取为对应的聚合对象。按照以上SQL读取出来的数据表的格式,类似于以下形式:
TableA TableB TableC TableD...
a1 b1 c1 d1
a1 b1 c2 NULL
a1 b2 c3 NULL
a1 b3 NULL NULL
a2 b4 c5 NULL
a2 b5 NULL NULL
a3 NULL NULL NULL
它是TableA的查询结果。对应每一个TableA的行,都有一个更小的表与之对应。如下图:
a1在整个大表中,对应红线框住的表。b1,b2,b3是它的关系对象,而对应b1的子表是绿线框住的更小的表,c1,c2是b1的关系对象。所以在读取这样的数据时,使用装饰模式定义了一个虚拟的IGTable:
/// <summary> /// 一个存储表格数据的对象 /// /// 注意: /// 以此为参数的方法只能在服务端执行 /// </summary> public interface IGTable { /// <summary> /// 行数 /// </summary> int Count { get; } /// <summary> /// 获取指定的行。 /// </summary> /// <param name="rowIndex"></param> /// <returns></returns> DataRow this[int rowIndex] { get; } }
GTable是从DataTable适配到IGTable的“适配器”。SubTable表示某一个IGTable的子表。定义如下:
/// <summary> /// 封装了DataRowCollection的一般Table /// </summary> public class GTable : IGTable { private DataRowCollection _table; public GTable(DataTable table){…} }
/// <summary> /// 这是个子表格。 /// /// 它表示的是某一表格中的一些指定的行。 /// </summary> public class SubTable : IGTable { private IGTable _table; private int _startRow; private int _endRow; /// <summary> /// 构造一个指定table的子表。 /// </summary> /// <param name="table"></param> /// <param name="startRow">这个表在table中的开始行。</param> /// <param name="endRow">这个表在table中的结束行。</param> public SubTable(IGTable table, int startRow, int endRow){…} }
定义好被读取的数据的结构后,按照刚才划分子表的逻辑,并调用T GEntity<T>.ReadDataDirectly(DataRow)方法生成所有对象即可:
public static class EntityListHelper { /// <summary> /// 这个方法把table中的数据全部读取并转换为对象存入对象列表中。 /// /// 算法简介: /// 由于子对象的数据都是存储在这个IGTable中,所以每一个TEntity可能对应多个行, /// 每一行数据其实就是一个子对象的数据,而TEntity的属性值是重复的。 /// 所以这里找到每个TEntity对应的第一行和最后一行,把它封装为一个子表格,传给子对象集合进行加载。 /// 这样的设计是为了实现重用这个方法:集合加载IGTable中的数据。 /// </summary> /// <typeparam name="TCollection"></typeparam> /// <typeparam name="TEntity"></typeparam> /// <param name="list">转换的对象存入这个列表中</param> /// <param name="table"> /// 表格数据,数据类型于以下形式: /// TableA TableB TableC TableD... /// a1 b1 c1 /// a1 b1 c2 /// a2 b2 c3 /// a2 b3 NULL /// a3 NULL NULL /// ... /// </param> /// <param name="relationLoader"> /// 为每个TEntity调用此方法,从IGTable中加载它对应的孩子对象。 /// 加载完成后的对象会被加入到list中,所以此方法有可能返回一个全新的TEntity。 /// </param> public static void ReadFromTable<TCollection, TEntity>(TCollection list, IGTable table, Func<TEntity, IGTable, TEntity> relationLoader) where TCollection : GBusinessListBase<TCollection, TEntity> where TEntity : GEntity<TEntity> { list.RaiseListChangedEvents = false; Guid? lastId = null; //每个TEntity对象对应的第一行数据 int startRow = 0; for (int i = 0, c = table.Count; i < c; i++) { var row = table[i]; string idName = GEntity<TEntity>.GetReadableColumnSql("Id"); var objId = row[idName]; Guid? id = objId != DBNull.Value ? (Guid)objId : (Guid?)null; //如果 id 改变,表示已经进入到下一个 TEntity 对象的开始行了。 if (id != lastId) { //不是第一次 if (lastId.HasValue) { //前一行就是最后一行。 int endRow = i - 1; TEntity item = CreateEntity<TEntity>(table, startRow, endRow, relationLoader); list.Add(item); //重置 startRow 为下一个 TEntity startRow = i; } } lastId = id; } //加入最后一个 Entity if (lastId.HasValue) { TEntity lastEntity = CreateEntity<TEntity>(table, startRow, table.Count - 1, relationLoader); list.Add(lastEntity); } //完毕,退出 list.RaiseListChangedEvents = true; } /// <summary> /// 把 table 从 startRow 到 endRow 之间的数据,都转换为一个 TEntity 并返回。 /// </summary> private static TEntity CreateEntity<TEntity>(IGTable table, int startRow, int endRow, Func<TEntity, IGTable, TEntity> relationLoader) where TEntity : GEntity<TEntity> { //新的TEntity TEntity item = GEntity<TEntity>.ReadDataDirectly(table[startRow]); Debug.Assert(item != null, "id不为空,对象也不应该为空。"); var childTable = new SubTable(table, startRow, endRow); item = relationLoader(item, childTable); return item; } } 这段代码中最关键的地方是relationLoader的定义,这个方法传入一个Entity和其对应的所有行,由它自己再来调用关系对象类的方法读取行并生成最后的Entity返回。在后面,我会给出一个较完事的例子。
工厂方法-命名约定:
其实,Linq To Sql 已经提供了API支持此类操作:LoadWith,AssociateWith。在使用它作为数据层的应用中,可以轻松的实现聚合加载。但是当你处在多层应用中时,为了不破坏数据访问层的封装性,该层接口的设计是不会让上层知道目前在使用何种ORM框架进行查询。可是,数据层到底要加载哪些关系数据,又必须由上层的客户程序在接口中以某种形式进行标注。为了让数据层的接口设计保持语意的明朗,我们可以考虑使用和LinqToSql相同的方案,使用表达式作为接口的参数。这样,在使用的时候,可以这样写:
Expression<Func<Article,Object>> loadOptions = a => a.User;
ArticlesRepository.Get(new PagerInfo(), loadOptions)
但是,LinqToSql、EF等框架虽然能提高开发效率,但是性能却不好,特别是Web项目,更是要谨慎用之。我推荐在项目上线的前期使用它们,因为这时候性能要求不高,而人力资源又比较紧张;而当性能要求较高时,再优化库,换为高效率的SQL实现查询。
按照上面的设计,当后期项目不再使用ORM框架,而使用SQL/存储过程实现接口时,要实现如ArticlesRepository.Get(Expression<Func<Article,Object>> loadOptions)一样灵活的接口,是件非常困难的事!而且其实上次使用的场景比较少,不会使用如此“宽广”的接口。所以我们在这里对接口的功能进行了限制,不需要为有限的查询设计无限的接口。在我们的项目中,使用如下的命名约定来定义方法:
GetArticles_With_User
GetPBSTypes_With_PBSTree
同时,在注释上写明此方法查询出的对象所附带的关系对象。
例子
我现在给出一个较完整的加载过程的代码,这个代码是GIX4项目中的实例:
数据访问层:
//此方法在客户端执行。
public static PBSs GetListByPBSTypeId_With_Properties(Guid pbsTypeId) {
//开始调用远程对象的DataPortal_Fetch方法 return DataPortal.Fetch<PBSs>(new GetListCriteria_With_Properties(pbsTypeId)); }
//以下所有方法在服务端执行
private void DataPortal_Fetch(GetListCriteria_With_Properties criteria) { var pbsTypeId = criteria.PBSTypeId; var sql = string.Format(SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES, pbsTypeId); using (var db = Helper.CreateDb()) { IGTable table = db.QueryTable(sql); this.ReadFromTable(table, PBS.GetChild_With_Properties); } }
注意到传入的委托是PBS.GetChild_With_Properties,正是上文提到的relationLoader,接着看:
public class PBS : GTreeEntity<PBS>, IDisplayModel { public static PBS GetChild_With_Properties(PBS pbs, IGTable subTable) { pbs = GetChild(pbs);//获取一个新的对象,并从参数中拷贝数据。
//同时,加载PBS的Properties属性。 var properties = PBSPropertys.GetChild_WithValues(pbs, subTable); pbs.LoadProperty(PBSPropertysProperty, properties); return pbs; } }
public class PBSPropertys : GEntityTreeList<PBSPropertys, PBSProperty> { public static PBSPropertys GetChild_WithValues(PBS pbs, IGTable table) { var properties = GetChild(); properties.ReadFromTable(table, PBSProperty.GetChild_With_Values); foreach (var property in properties) { property.PBS = pbs; } //排序 var result = GetChild(); result.AddRange(properties.OrderBy(p => p.OrderNo)); return result; } }
public class PBSProperty : GEntity<PBSProperty>, ITreeNode, IOrderedObject { public static PBSProperty GetChild_With_Values(PBSProperty property, IGTable table) { var model = GetChild(property); var values = PBSPropertyOptionalValues.GetChild(model, table); model.LoadProperty(PBSPropertyOptionalValuesProperty, values); return model; } }
public partial class PBSPropertyOptionalValues : GEntityList<PBSPropertyOptionalValues, PBSPropertyOptionalValue> { internal static PBSPropertyOptionalValues GetChild(PBSProperty parent, IGTable table) { var values = GetChild(); values.ReadFromTable(table); foreach (var value in values) { value.PBSProperty = parent; } return values; } }
调用关系如下:
客户程序调用方法如下:
var pbsList = PBSs.GetListByPBSTypeId_With_Properties(id); foreach (var pbs in pbsList) { foreach (var property in pbs.PBSPropertys) { foreach (var optionalValue in property.PBSPropertyOptionalValues) { //...... } } }
这里虽然客户程序使用了多次循环,但是由于在获取数据时我们已经指定了,在加载PBS的时候,把每个PBS的PBS属性和属性值都带上,所以这里也只有一次数据/远程访问。
使用场景
聚合SQL优化查询次数的模式,已经被我在多个项目中使用过。它一般被使用在对项目进行重构/优化的场景中。原因是:在一开始编写数据层代码时,其中我们不知道上层在使用时会需要它的哪些关系对象。只有当某个业务逻辑的流程写完了,然后再对它进行分析时,才会发现它在一次执行过程中,到底需要哪些数据。这时,如果需要对它进行优化,我们就可以有的放矢地写出聚合SQL,并映射为带有关系的对象了。
小结
本节主要讲了GIX4中的聚合SQL的应用。
下一节开始讲在本次优化过程中,使用的另一个技术:预加载。主要说下我们的预加载需求及对应的API设计,可能会附带说下.NET4.0并行库在系统中的应用。
20110107
新的聚合SQL的API: