ORM查询语言(OQL)简介高级篇
2013-07-27 10:08
309 查看
ORM查询语言(OQL)简介--高级篇:脱胎换骨
在写本文之前,一直在想文章的标题应怎么取。在写了《ORM查询语言(OQL)简介--概念篇》、《ORM查询语言(OQL)简介--实例篇》之后,觉得本篇文章应该是前2篇的延续,但又不是一般的延续,因为今天要写的这篇内容,是基于对框架OQL完全重构之后来写的,所以加上一个副标题:脱胎换骨!一、OQL之前生
1.1,内容回顾:
OQL是我设计用来处理PDF.NET开发框架的ORM查询的,因此叫做ORM查询语言。自2006年第一版以来,经历了多次重构,到PDF.NET Ver 4.X 版本,已经比较稳定了,在我做的项目和框架用户朋友的项目中得到成功应用,基本符合一般的常规应用需求。OQL有下面3个显著特点:
抽象的SQL,屏蔽了具体数据库的差异,因此支持所有数据库;
对象化的“SQL”,写OQL代码能够获得IDE的智能提示,能够得到编译时检查确保不会写出错误的SQL;
没有使用.NET的特性,比如泛型、反射、表达式树等东西,因此理论上OQL可以跨语言平台,比如移植到Java,C++,VB等。
OQL的原理基于2大特性:
表达式的链式调用
属性的实例调用
OQL支持4大类数据操作
数据查询:
单实体类(单表)查询
多实体类(多表)关联查询
数据修改
更新数据
删除数据
统计、聚合运算
OQL分页
1.2,老版本的局限
尽管OQL已经可以解决80%的查询需求,剩下的20%查询需求我都建议框架用户使用SQL-MAP技术来完成,但对于用户而言,是不太愿意从ORM模式切换到SQL模式的,希望OQL能够解决尽可能多的查询需求。那么,PDF.NET Ver 4.X 版本的OQL有哪些不足呢?1.2.1,自连接查询
也称为表自身连接查询。OQL支持多表(实体)查询的,但却无法支持自连接查询,原因是自连接查询必须指定表的别名:SELECT R1.readerid,R1.readername,R1.unit,R1.bookcount FROM ReaderInfo AS R1,ReaderInfo AS R2 WHERE R2.readerid=9704 AND R1.bookcount>R2.bookcount --连接关系 ORDER BY R1.bookcount
上面这个查询是从ReaderInfo表查询可借图书数目比编号为9704读者多的所有读者信息,这里对表使用了别名来实现的,如果不使用别名,那么这个查询就无法实现。而OQL之前的版本,是不支持表的别名的,因此,对于连接查询,OQL生成的可能是这样子的SQL语句:
SELECT teacher.*,student.* FROM teacher INNER JOIN student ON teacher.id=student.tea_id
1.2.2,子查询
老版本OQL仅支持IN条件的子查询,不能像SQL那么灵活的进行各种子查询,其实不支持的原因其中一个也是因为OQL查询不支持表的别名,另外一个原因是子查询无法获取到父查询的表名和字段名。子查询是一个很常用的功能,如果不能够支持,那么就大大限制了OQL的使用范围。下面是来自SQLSERVER 联机帮助的说明:
子查询也称为内部查询或内部选择,而包含子查询的语句也称为外部查询或外部选择。
许多包含子查询的 Transact-SQL 语句都可以改用联接表示。其他问题只能通过子查询提出。在 Transact-SQL 中,包含子查询的语句和语义上等效的不包含子查询的语句在性能上通常没有差别。但是,在一些必须检查存在性的情况中,使用联接会产生更好的性能。否则,为确保消除重复值,必须为外部查询的每个结果都处理嵌套查询。所以在这些情况下,联接方式会产生更好的效果。以下示例显示了返回相同结果集的 SELECT 子查询和 SELECT 联接:
PropertyGettingEventArgs
而OQL实例对象,正是订阅了EventHandler<PropertyGettingEventArgs> 事件:
public OQL(EntityBase e) { currEntity = e; //其它代码略 e.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(e_PropertyGetting); } void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { //其它代码略 }
所以,在属性获取事件中,我们可以通过PropertyGettingEventArgs.PropertyName 得到实体类属性对应的字段名称,因此,我们就可以方便的做到选取我们本次查询需要的字段,例如下面的OQL查询:
Users user = new Users(); OQL q0 = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END;
对应的SQL语句:
SELECT [ID], [UserName], [RoleID] FROM [LT_Users]
这样,我们无需使用委托,也不需要Lambda表达式,更不需要表达式树,就能够直接获取到要查询的表名称和字段名称,写法比Linq更简洁,处理速度更快速。当然,代价是要先实例化实体类。
2.1,属性获取事件的变化
在事件方法e_PropertyGetting 中,我们看看PDF.NET Ver 5.0前后的变化:Ver 4.X 以前:
void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { doPropertyGetting(((EntityBase)sender).TableName, e.PropertyName); } private void doPropertyGetting(string tableName, string propertyName) { if (isJoinOpt) { string propName = "[" + tableName + "].[" + propertyName + "]"; this.currJoinEntity.AddJoinFieldName(propName); } else { string field = this.joinedString.Length > 0 ? tableName + "].[" + propertyName : propertyName; if (!hasSelected && !selectedFields.Contains(field)) selectedFields.Add(field); } this.getingTableName = tableName; this.getingPropertyName = propertyName; }
在doPropertyGetting 方法中,区分是否有实体类连接查询,来处理不同的表名称和字段名称,这里看到连接查询的时候没有为表加上别名,而是直接使用了“表名称.字段名称”这种表示字段的形式。同时,将当前获取到的表字段名,马上赋值给getingPropertyName 变量。这带来了一个问题,属性字段名称必须马上被使用,否则就会出问题。
由于不同的情况使用属性字段的时机不一样,为了处理这些不同的情况加入了各种Case下的处理代码,比如将Select方法要使用的属性字段名称保存到列表 selectedFields 中。这种处理方法无疑大大增加了代码的复杂度。
Ver 5.0 版本的改进
前面说到属性获取到的属性字段名称必须马上被使用,否则就会出问题。如果我们不论何种情况,都将这个属性字段名先保存起来再使用呢?使用队列?链表?堆栈?这些集合都可以,但在编译原理中,对表达式的处理都是使用堆栈来做的,其中必有它的好处,以后会体会到。
下面是属性获取事件代码:
void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { TableNameField tnf = new TableNameField() { Field = e.PropertyName, Entity = (EntityBase)sender, Index=this.GetFieldGettingIndex() }; fieldStack.Push(tnf); }
这里直接将属性字段名存在TablenameField 结构的Field字段中,然后将这个结构压入堆栈对象fieldStack 中,需要的时候在从堆栈中弹出最新的一个 TableNameField 结构。这样,不论是OQL的Select方法,Where方法还是OrderBy方法,都能够使用统一的堆栈结构来获取方法使用的属性字段了。
2.3,统一属性获取事件
除了OQL本身需要“属性获取事件”,OQL关联的OQLCompare对象,OQLOrder对象,都需要处理属性获取事件,比如之前实例化OQLCompare对象:/// <summary> /// 使用一个实体对象初始化本类 /// </summary> /// <param name="e"></param> public OQLCompare(EntityBase e) { this.CurrEntity = e; //this.CurrEntity.ToCompareFields = true; this.CurrEntity.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); } /// <summary> /// 使用多个实体类进行连接查询的条件 /// </summary> /// <param name="e"></param> /// <param name="joinedEntitys"></param> public OQLCompare(EntityBase e, params EntityBase[] joinedEntitys) { this.CurrEntity = e; this.CurrEntity.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); //处理多个实体类 if (joinedEntitys != null && joinedEntitys.Length > 0) { this.joinedEntityList = new List<EntityBase>(); foreach (EntityBase item in joinedEntitys) { this.joinedEntityList.Add(item); item.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); } } }
属性获取事件处理方法:
void CurrEntity_PropertyGetting(object sender, PropertyGettingEventArgs e) { if (this.joinedEntityList != null) { this.currPropName = "[" + ((EntityBase)sender).TableName + "].[" + e.PropertyName + "]"; } else { this.currPropName = "[" + e.PropertyName + "]"; //propertyList.Add(e.PropertyName); } }
之所以要在OQLCompare等对象中也要处理属性获取事件,是为了OQLCompare能够独立使用,但这带来一些问题:
各地的属性获取事件处理代码类似,代码有冗余;
没有体现出OQL跟OQLCompare 、OQLOrder对象之见的聚合性,呈现出松散的结构,因此可能出现OQLCompare使用的实体类在OQL中没有使用,从而产生错误的查询;
OQLCompare中的的字段名与OQL缺乏相关性,因此只能通过“表名称.字段名称”这种形式来使用属性字段名,无法使用别名。
Ver 5.0的解决办法:
在OQL对象上,定义一些方法供OQL的关联子对象来访问需要的属性字段名信息:
/// <summary> /// 从堆栈上只取一个字段名 /// </summary> /// <returns></returns> protected internal string TakeOneStackFields() { //其它代码略 TableNameField tnf = fieldStack.Pop(); return GetOqlFieldName(tnf); }
2.4,SQL的语法结构
SQL是结构化查询语言,它自身也是非常结构化的,每一个查询都有固定的语法结构,以Select为例,它可以有多种形式的写法:SELECT Field1,Field2... FROM