您的位置:首页 > 数据库

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

2010-06-25 18:44 477 查看
本节主要介绍,在GIX4系统中,如何应用上篇讲的方案来改善性能,如果与现有的系统环境集成在一起。大致包含以下内容:

SQL的生成

映射-数据读取方案

工厂方法-接口的命名约定

实例代码

SQL生成
GIX4系统中的所有领域模型及分布式访问机制,使用CSLA作为底层框架。而ORM机制,使用了一个非常轻量级的开源代码LiteORM实现。模型类的定义,采用以下的格式:



可以看到,在类的元数据定义中(这里目前使用的是Attribute的形式),已经包含了对应数据表和列的信息。所以为SQL的自动化自成提供了一定的支持。
其实,由于目前对性能要求比较高的模块少,所以用于优化查询的SQL主要还是依靠人工手写。但是由于LiteORM框架的功能比较有限,所以这里查询出来的表格数据需要由我们自己来进行读取并封装对象。考虑到:1.多表连接时,列名可能会重复;2.添加/删除列时,不要更改手写的SQL。所以至少列名应该自动生成,并不重复。我们把生成列名SQL的API都放在了所有模型的基类GEntity<T>中,如下:
[Serializable]
publicabstractclassGEntity<T>:Entity<T>
whereT:GEntity<T>
{
#region直接与数据行进行交互
///<summary>
///直接从数据集中取数据。
///
///注意:
///数据集中的列字段约定为:“表名_列名”,如“PBS_Name”。
///默认使用反射创建对象并读取数据!同“LiteORM”。
///
///意义:
///由于各个类的列名不再相同,所以这个方法的意义在于可以使用一句复杂的组合SQL加载一个聚合对象!
///</summary>
///<paramname="rowData">
///这个数据集中的列字段约定为:“表名_列名”,如“PBS_Name”。
///</param>
///<returns>
///如果id值为null,则返回null。
///</returns>
publicstaticTReadDataDirectly(DataRowrowData)

///<summary>
///获取可用于ReadDirectly方法读取的列名表示法。如:
///PBS.IdasPBS_Id,PBS.NameasPBS_Name,........
///</summary>
///<returns></returns>
publicstaticstringGetReadableColumnsSql()

///<summary>
///获取可用于ReadDirectly方法读取的列名表示法。如:
///p.IdasPBS_Id,p.NameasPBS_Name,........
///</summary>
///<paramname="tableAlias">表í的?别e名?</param>
///<returns></returns>
publicstaticstringGetReadableColumnsSql(stringtableAlias)

///<summary>
///获取columnName在DataRow中使用时约定的列名。
///</summary>
///<paramname="columnName"></param>
///<returns></returns>
publicstaticstringGetReadableColumnSql(stringcolumnName)

publicstaticITableGetTableInfo()
{
//这里加载表信息时,可能需要和服务器交互。
if(Helper.IsOnServer())
{
returnGetTableInfo_OnServer();
}
else
{
returnGetTableInfo_OnClient();
}
}
#endregion


例如,一个比较简单的聚合SQL如下:
privatestaticreadonlystringSQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES=string.Format(@"
select
{0},
{1},
{2}
fromPBSpbs
leftouterjoinPBSPropertyponpbs.Id=p.PBSId
leftouterjoinPBSPropertyOptionalValuevonp.Id=v.PBSPropertyId
wherepbs.PBSTypeId='{{0}}'
orderbypbs.Id,p.Id"
,PBS.GetReadableColumnsSql()
,PBSProperty.GetReadableColumnsSql("p")
,PBSPropertyOptionalValue.GetReadableColumnsSql("v"));


这个SQL格式生成的结果存储在静态字段中,不需要每次都生成。最后生成的SQL语句如下:
select
pbs.pidaspbs_pid,pbs.pbstypeidaspbs_pbstypeid,pbs.codeaspbs_code,pbs.nameaspbs_name,pbs.descriptionaspbs_description,pbs.ordernoaspbs_orderno,pbs.idaspbs_id,
p.pbsidaspbsproperty_pbsid,p.codeaspbsproperty_code,p.ordernoaspbsproperty_orderno,p.pidaspbsproperty_pid,p.nameaspbsproperty_name,p.unitaspbsproperty_unit,p.valuetypeaspbsproperty_valuetype,p.editortypeaspbsproperty_editortype,p.calcitemtypeaspbsproperty_calcitemtype,p.isusedtoqueryaspbsproperty_isusedtoquery,p.isrequiredaspbsproperty_isrequired,p.isusedtosummarizeaspbsproperty_isusedtosummarize,p.idaspbsproperty_id,
v.pbspropertyidaspbspropertyoptionalvalue_pbspropertyid,v.valueaspbspropertyoptionalvalue_value,v.descriptionaspbspropertyoptionalvalue_description,v.idaspbspropertyoptionalvalue_id
fromPBSpbs
leftouterjoinPBSPropertyponpbs.Id=p.PBSId
leftouterjoinPBSPropertyOptionalValuevonp.Id=v.PBSPropertyId
wherepbs.PBSTypeId='{0}'
orderbypbs.Id,p.Id

映射-数据读取方案
SQL已经生成了,接下来就是把整个一张大表读取为对应的聚合对象。按照以上SQL读取出来的数据表的格式,类似于以下形式:
TableATableBTableCTableD...
a1b1c1d1
a1b1c2NULL
a1b2c3NULL
a1b3NULLNULL
a2b4c5NULL
a2b5NULLNULL
a3NULLNULLNULL
它是TableA的查询结果。对应每一个TableA的行,都有一个更小的表与之对应。如下图:



a1在整个大表中,对应红线框住的表。b1,b2,b3是它的关系对象,而对应b1的子表是绿线框住的更小的表,c1,c2是b1的关系对象。所以在读取这样的数据时,使用装饰模式定义了一个虚拟的IGTable:


///<summary>
///一个存储表格数据的对象
///
///注意:
///以此为参数的方法只能在服务端执行
///</summary>
publicinterfaceIGTable
{
///<summary>
///行数
///</summary>
intCount{get;}

///<summary>
///获取指定的行。
///</summary>
///<paramname="rowIndex"></param>
///<returns></returns>
DataRowthis[introwIndex]{get;}
}

GTable是从DataTable适配到IGTable的“适配器”。SubTable表示某一个IGTable的子表。定义如下:
///<summary>
///封装了DataRowCollection的一般Table
///</summary>
publicclassGTable:IGTable
{
privateDataRowCollection_table;

publicGTable(DataTabletable){…}
}
///<summary>
///这是个子表格。
///
///它表示的是某一表格中的一些指定的行。
///</summary>
publicclassSubTable:IGTable
{
privateIGTable_table;
privateint_startRow;
privateint_endRow;

///<summary>
///构造一个指定table的子表。
///</summary>
///<paramname="table"></param>
///<paramname="startRow">这个表在table中的开始行。</param>
///<paramname="endRow">这个表在table中的结束行。</param>
publicSubTable(IGTabletable,intstartRow,intendRow){…}
}
定义好被读取的数据的结构后,按照刚才划分子表的逻辑,并调用TGEntity<T>.ReadDataDirectly(DataRow)方法生成所有对象即可:
publicstaticclassEntityListHelper
{
///<summary>
///这个方法把table中的数据全部读取并转换为对象存入对象列表中。
///
///算法简介:
///由于子对象的数据都是存储在这个IGTable中,所以每一个TEntity可能对应多个行,
///每一行数据其实就是一个子对象的数据,而TEntity的属性值是重复的。
///所以这里找到每个TEntity对应的第一行和最后一行,把它封装为一个子表格,传给子对象集合进行加载。
///这样的设计是为了实现重用这个方法:集合加载IGTable中的数据。
///</summary>
///<typeparamname="TCollection"></typeparam>
///<typeparamname="TEntity"></typeparam>
///<paramname="list">转换的对象存入这个列表中</param>
///<paramname="table">
///表格数据,数据类型于以下形式:
///TableATableBTableCTableD...
///a1b1c1
///a1b1c2
///a2b2c3
///a2b3NULL
///a3NULLNULL
///...
///</param>
///<paramname="relationLoader">
///为每个TEntity调用此方法,从IGTable中加载它对应的孩子对象。
///加载完成后的对象会被加入到list中,所以此方法有可能返回一个全新的TEntity。
///</param>
publicstaticvoidReadFromTable<TCollection,TEntity>(TCollectionlist,IGTabletable,Func<TEntity,IGTable,TEntity>relationLoader)
whereTCollection:GBusinessListBase<TCollection,TEntity>
whereTEntity:GEntity<TEntity>
{
list.RaiseListChangedEvents=false;

Guid?lastId=null;
//每个TEntity对象对应的第一行数据
intstartRow=0;
for(inti=0,c=table.Count;i<c;i++)
{
varrow=table[i];

stringidName=GEntity<TEntity>.GetReadableColumnSql("Id");
varobjId=row[idName];
Guid?id=objId!=DBNull.Value?(Guid)objId:(Guid?)null;

//如果id改变,表示已经进入到下一个TEntity对象的开始行了。
if(id!=lastId)
{
//不是第一次
if(lastId.HasValue)
{
//前一行就是最后一行。
intendRow=i-1;
TEntityitem=CreateEntity<TEntity>(table,startRow,endRow,relationLoader);
list.Add(item);
//重置startRow为下一个TEntity
startRow=i;
}
}
lastId=id;
}

//加入最后一个Entity
if(lastId.HasValue)
{
TEntitylastEntity=CreateEntity<TEntity>(table,startRow,table.Count-1,relationLoader);
list.Add(lastEntity);
}

//完毕,退出
list.RaiseListChangedEvents=true;
}

///<summary>
///把table从startRow到endRow之间的数据,都转换为一个TEntity并返回。
///</summary>
privatestaticTEntityCreateEntity<TEntity>(IGTabletable,intstartRow,intendRow,Func<TEntity,IGTable,TEntity>relationLoader)whereTEntity:GEntity<TEntity>
{
//新的TEntity
TEntityitem=GEntity<TEntity>.ReadDataDirectly(table[startRow]);
Debug.Assert(item!=null,"id不为空,对象也不应该为空。");

varchildTable=newSubTable(table,startRow,endRow);
item=relationLoader(item,childTable);
returnitem;
}
}
这段代码中最关键的地方是relationLoader的定义,这个方法传入一个Entity和其对应的所有行,由它自己再来调用关系对象类的方法读取行并生成最后的Entity返回。在后面,我会给出一个较完事的例子。
工厂方法-命名约定:

其实,LinqToSql已经提供了API支持此类操作:LoadWith,AssociateWith。在使用它作为数据层的应用中,可以轻松的实现聚合加载。但是当你处在多层应用中时,为了不破坏数据访问层的封装性,该层接口的设计是不会让上层知道目前在使用何种ORM框架进行查询。可是,数据层到底要加载哪些关系数据,又必须由上层的客户程序在接口中以某种形式进行标注。为了让数据层的接口设计保持语意的明朗,我们可以考虑使用和LinqToSql相同的方案,使用表达式作为接口的参数。这样,在使用的时候,可以这样写:

Expression<Func<Article,Object>>loadOptions=a=>a.User;

ArticlesRepository.Get(newPagerInfo(),loadOptions)

但是,LinqToSql、EF等框架虽然能提高开发效率,但是性能却不好,特别是Web项目,更是要谨慎用之。我推荐在项目上线的前期使用它们,因为这时候性能要求不高,而人力资源又比较紧张;而当性能要求较高时,再优化库,换为高效率的SQL实现查询。

按照上面的设计,当后期项目不再使用ORM框架,而使用SQL/存储过程实现接口时,要实现如ArticlesRepository.Get(Expression<Func<Article,Object>>loadOptions)一样灵活的接口,是件非常困难的事!而且其实上次使用的场景比较少,不会使用如此“宽广”的接口。所以我们在这里对接口的功能进行了限制,不需要为有限的查询设计无限的接口。在我们的项目中,使用如下的命名约定来定义方法:

GetArticles_With_User

GetPBSTypes_With_PBSTree

同时,在注释上写明此方法查询出的对象所附带的关系对象。

例子

我现在给出一个较完整的加载过程的代码,这个代码是GIX4项目中的实例:

数据访问层:

//此方法在客户端执行。

publicstaticPBSsGetListByPBSTypeId_With_Properties(GuidpbsTypeId)
{
//开始调用远程对象的DataPortal_Fetch方法
returnDataPortal.Fetch<PBSs>(newGetListCriteria_With_Properties(pbsTypeId));
}
//以下所有方法在服务端执行
privatevoidDataPortal_Fetch(GetListCriteria_With_Propertiescriteria)
{
varpbsTypeId=criteria.PBSTypeId;
varsql=string.Format(SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES,pbsTypeId);

using(vardb=Helper.CreateDb())
{
IGTabletable=db.QueryTable(sql);
this.ReadFromTable(table,PBS.GetChild_With_Properties);
}
}
注意到传入的委托是PBS.GetChild_With_Properties,正是上文提到的relationLoader,接着看:
publicclassPBS:GTreeEntity<PBS>,IDisplayModel
{
publicstaticPBSGetChild_With_Properties(PBSpbs,IGTablesubTable)
{
pbs=GetChild(pbs);//获取一个新的对象,并从参数中拷贝数据。
//同时,加载PBS的Properties属性。
varproperties=PBSPropertys.GetChild_WithValues(pbs,subTable);
pbs.LoadProperty(PBSPropertysProperty,properties);

returnpbs;
}
}
publicclassPBSPropertys:GEntityTreeList<PBSPropertys,PBSProperty>
{
publicstaticPBSPropertysGetChild_WithValues(PBSpbs,IGTabletable)
{
varproperties=GetChild();

properties.ReadFromTable(table,PBSProperty.GetChild_With_Values);

foreach(varpropertyinproperties)
{
property.PBS=pbs;
}

//排序
varresult=GetChild();
result.AddRange(properties.OrderBy(p=>p.OrderNo));

returnresult;
}
}
publicclassPBSProperty:GEntity<PBSProperty>,ITreeNode,IOrderedObject
{
publicstaticPBSPropertyGetChild_With_Values(PBSPropertyproperty,IGTabletable)
{
varmodel=GetChild(property);

varvalues=PBSPropertyOptionalValues.GetChild(model,table);
model.LoadProperty(PBSPropertyOptionalValuesProperty,values);

returnmodel;
}
}
publicpartialclassPBSPropertyOptionalValues:GEntityList<PBSPropertyOptionalValues,PBSPropertyOptionalValue>
{
internalstaticPBSPropertyOptionalValuesGetChild(PBSPropertyparent,IGTabletable)
{
varvalues=GetChild();

values.ReadFromTable(table);

foreach(varvalueinvalues)
{
value.PBSProperty=parent;
}

returnvalues;
}
}

调用关系如下:





客户程序调用方法如下:

varpbsList=PBSs.GetListByPBSTypeId_With_Properties(id);
foreach(varpbsinpbsList)
{
foreach(varpropertyinpbs.PBSPropertys)
{
foreach(varoptionalValueinproperty.PBSPropertyOptionalValues)
{
//......
}
}
}

这里虽然客户程序使用了多次循环,但是由于在获取数据时我们已经指定了,在加载PBS的时候,把每个PBS的PBS属性和属性值都带上,所以这里也只有一次数据/远程访问。

使用场景

聚合SQL优化查询次数的模式,已经被我在多个项目中使用过。它一般被使用在对项目进行重构/优化的场景中。原因是:在一开始编写数据层代码时,其中我们不知道上层在使用时会需要它的哪些关系对象。只有当某个业务逻辑的流程写完了,然后再对它进行分析时,才会发现它在一次执行过程中,到底需要哪些数据。这时,如果需要对它进行优化,我们就可以有的放矢地写出聚合SQL,并映射为带有关系的对象了。

小结

本节主要讲了GIX4中的聚合SQL的应用。

下一节开始讲在本次优化过程中,使用的另一个技术:预加载。主要说下我们的预加载需求及对应的API设计,可能会附带说下.NET4.0并行库在系统中的应用。

20110107

新的聚合SQL的API:

OEA框架-优化聚合SQL
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: