ORM查询语言(OQL)简介--高级篇(续):庐山真貌
相关文章内容索引:
PDF.NET框架的OQL经过“脱胎换骨”般的重构之后,引来了它华丽丽的新篇章,将“对象化的SQL”特征发挥到极致,与至于我在Q群里面说这应该算是OQL的“收山之作”了。然而,我这么说有什么依据?它的设计哲学是什么?它究竟是何样?由于本文篇幅较长,请听本篇慢慢道来,叙说它的庐山真面目!
[有图有真相]
User user=new User();
注意:图上的表达式中的形参 parent 其实是OQL对象,这里表示父级OQL对象,它参与构造它的子OQL对象。
(图4:高级子查询)
(图5:SQL锁)
三、精简之道
PDF.NET诞生的背景就是在2006年,我参与一个使用iBatis.Net的项目,当时没有找到合适的代码生成工具,手工写各种配置,写SQL映射写的吐血,心想这么复杂的东西为何还得到那么多人的喜欢?也许功能的确很强大,而复杂正是体现了它的强大,而我,天生就是一个只喜欢“简单”的程序猿,因此选择了.NET而不是Java平台,既然如此何必要用iBatis这种复杂的东西?
于是,我参考iBatis的特点,将它大力精简,仅保留了SQL简单的映射功能,将SQL语句写到一个配置文件里面,然后提供一套生成工具来自动生成DAL代码,这便是PDF.NET的SQL-MAP功能。尽管从使用上已经比iBatis.Net简单很多了,但是对于大多数简单的CRUD,还是需要写SQL映射,实在不爽,于是给框架加入了点ORM功能,但觉得当时的ORM动不动就将实体类的全部属性字段的数据返回来,也觉得不爽,于是设计了OQL,来操作ORM。这便是PDF.NET Ver 1.0 诞生的故事。
尽管已经过去7年多时间,PDF.NET不断发展,但主线还是它的特色功能SQL-MAP、OQL、数据控件这三大部分。最近一段时间,我对OQL进行了完全的重构,仍然坚守最初的设计理念,做最简单最易用的数据框架。下面,就从OQL的查询API设计,来讲下这个理念。
3.1,Select血案—委托之殇
ORM选取实体字段的Select方法该怎样设计?象EF这样子:
var user = from c in db.User select new { c.UserID, c.Accounts, c.Point, c.SavePoint, c.LoginServerID };
EF使用Linq作为ORM查询语言,Linq是语法糖,它本质上会转化成下面的Lambda方式:
Var user=db.User.Select(c=>new { c.UserID, c.Accounts, c.Point, c.SavePoint, c.LoginServerID });
而Lambda,常常用来简化匿名委托的写法,也就是说,Lambda也是委托的一种语法糖。因此,EF实现这个效果,靠的还是委托这个功能。
上面是单个实体类的实体属性字段选取,如果是多个呢?
我们知道,Linq可以处理多个实体类的连接(左、右、内)查询,但百度了半天,实在不知道怎么用Lambda来实现多个实体类的属性选取,有知道的还请大侠告之,谢谢。
假设有一个集合C,它内部包含了2个集合A,B连接处理后的数据,我们这样来使用Select方法:
Var data=C.Select((a,b)=>new { F1=a.F1, F2=b.F2 });
大家注意到Select方法需要传递2个参数进去,此时对参数的类型推导可能会成为问题,因此,实际上的Select扩展方法的定义应该带有2个类型的泛型方法的,调用的其实是下面的方法:
Var data=C.Select<A,B>((a,b)=>new { F1=a.F1, F2=b.F2 });
假如有3个类型的参数呢?自然得像下面这个样子使用:
Var data=C.Select<X,Y,Z>((x,y,z)=>new { F1=x.F1, F2=y.F2, F3=z.F3 });
假如还有4个、5个。。。。N个类型的参数呢?岂不是要定义含有N个参数的Select扩展方法?
如果还有其它方法,假设Where方法也有这种问题呢?
My God!
委托方法尽管保证了我们写的代码是强类型的,一旦遇到方法需要的类型过多那么麻烦也就越多,还是回过头来说ORM查询的select问题,假设使用委托的解决方案怎么看都不是一个最佳的方案,特别是多实体类查询的时候。
PDF.NET的ORM查询语言OQL很早就注意到了这个问题,所以它的Select方法采用了非泛型化的设计,例如单个实体类属性字段选取:
OQL q = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END;
多个实体类的属性字段选取:
OQL q2 = OQL.From(user) .InnerJoin(roles).On(user.RoleID, roles.ID) .Select(user.RoleID, roles.RoleName) .END;
从上面的例子看出来了,不管OQL查询几个实体,它的Select使用方式始终是一致的,要想使用哪个属性字段,在Select方法里面通过“实体类实例.属性”的方式,一直写下去即可,而支持这个功能仅使用了C#的可选参数功能:
public OQL1 Select(params object[] fields) { //具体实现略 }
方法没有使用委托参数,也没有定义N多泛型重载,就轻易地实现了我们的目标,从这个意义上来说,在这里使用委托,真是委托之殇啊!
3.2,Where迷途—委托神器
3.2.1,最简单的Where条件比较方法
OQL的Where 条件构造支持最简单的使用方式,如果查询条件都是“相等比较”方式,那么可以使用下面的方式:
Users user = new Users() { NickName = "pdf.net", RoleID=5 }; OQL q0 = OQL.From(user) .Select() .Where(user.NickName,user.RoleID) .OrderBy(user.ID) .END; q0.SelectStar = true; Console.WriteLine("q0:one table and select all fields \r\n{0}", q0); Console.WriteLine(q0.PrintParameterInfo());
程序输出的结果是:
q0:one table and select all fields SELECT * FROM [LT_Users] WHERE [NickName]=@P0 AND [RoleID]=@P1 ORDER BY [ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=5 Type:Int32 ------------------End------------------------
可见Where 这样使用非常简单,实际项目中很多也是想等条件查询的,采用这种方式不仅仅构造了查询参数,而且将参数值也顺利的设置好了,这就是使用ORM的实体类实例调用 方式最大的好处。
Where方法支持多个这样的实体类参数,该方法在PDF.NET Ver4.X之前就一直支持。下面是它的定义:
public OQL2 Where(params object[] fields) { //其它代码略 }
3.2.1,升级到V5版OQLCompare的问题
OQL的Where方法支持使用OQLCompare对象。在文章前面的2.6 OQLCompare –比较对象的组合模式 一节中说道我们通过它我们处理了长达5000行业务代码构造的查询条件,在PDF.NET Ver 4.X 版本中,OQLCompare对象得这样使用:
User user=New User(); OQLCompare cmp=new OQLCompare(user); OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”); If(xxx条件) { cmpResult=cmpResult & …… //其它条件 } //条件对象构造完成 OQL q=OQL.From(user).Select().Where(cmpResult).END;
如果前面的代码不修改,那么使用新版PDF.NET编译时候不会出错,但运行时会出错:
OQLCompare 关联的OQL对象为空!
3.2.2,OQLCompare新的构造函数
PDF.NET Ver 5.0版本之后,OQLCompare不通过实体类来初始化此对象,而是用对应的OQL对象来构造它,所以前面的代码需要改造成下面这个样子:
User user=New User(); OQL q=new OQL(user); OQLCompare cmp=new OQLCompare(q); OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”); If(xxx条件) { cmpResult=cmpResult & …… //其它条件 } //条件对象构造完成 q.Select().Where(cmpResult);
3.2.3,OQLCompare 条件比较委托
除了上面的修改方式,我们也可以不调整代码的顺序,仅作小小的改变:
User user=new User(); //OQLCompare cmp=new OQLCompare(user); OQLCompareFun cmpFun = cmp=> { OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”)
& cmp.Compare(user.Password,”=”,”123456”); if(xxx条件) { cmpResult=cmpResult & cmp.Compare(....)…… //AND其它条件 }
return cmpResult; } //条件对象构造完成 //OQL q=OQL.From(user).Select().Where(cmpResult).END; OQL q=OQL.From(user).Select().Where(cmpFun ).END;
这里,我们的Where方法接受了一个OQLCompareFun 委托参数,我们来看这个新版的Where方法是怎么实现的:
public OQL2 Where(OQLCompareFunc cmpFun) { OQLCompare compare = new OQLCompare(this.CurrentOQL); OQLCompare cmpResult = cmpFun(compare); return GetOQL2ByOQLCompare(cmpResult); }
原来没啥神器的代码,仅仅在方法内部声明了一个新的OQLCompare对象,它使用了传入OQL对象的构造函数,然后将这个OQLCompare对象实例给OQLCompare委托方法使用。但是,我们不能小看这个小小的改进,它将具体OQLCompare对象的处理延迟到了顶层OQL对象的实例化之后,这个“延迟执行”的特点,大大简化了我们原来的代码。
3.2.4,委托进阶--泛型委托
前面的代码还是稍微复杂了点,我们来看一个简单的例子:
User user=new User(); Var list=OQL.From (user) .Select() .Where(cmp=> cmp.Property( user.RoleId)==10) .END .ToList<User>();
我们仅使用了2行代码查询到了用户表中所有用户的角色ID等于10的记录,我们在一行代码之内完成了条件代码的编写。
对于前面的代码,我们还能不能继续简化呢?如果我们的Where用的委托参数能够接受一个实体类参数,那么User对象的实例不必在这里声明了。
下面是OQLCompare的委托方法定义:
public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p);
然后再定义一个对应的Where方法:
public OQL2 Where<T>(OQLCompareFunc<T> cmpFun) where T : EntityBase { OQLCompare compare = new OQLCompare(this.CurrentOQL); T p1 = GetInstance<T>(); OQLCompare cmpResult = cmpFun(compare, p1); return GetOQL2ByOQLCompare(cmpResult); }
OK,有了这个委托神器,前面的代码终于可以一行搞定了:
Var list=OQL.From<User>() .Select() .Where<User>((cmp,user)=> cmp.Property( user.RoleId)==10) .END .ToList<User>();
不错,委托真是厉害!
按照上面的思路如法炮制,我们定义最多有3个泛型类型的OQLCompare泛型委托,下面是全部的委托定义:
public delegate OQLCompare OQLCompareFunc(OQLCompare cmp); public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p); public delegate OQLCompare OQLCompareFunc<T1,T2>(OQLCompare cmp,T1 p1,T2 p2); public delegate OQLCompare OQLCompareFunc<T1, T2,T3>(OQLCompare cmp, T1 p1, T2 p2,T3 p3);
有了它,我们再来看一个复杂点的例子:
void Test4() { OQLCompareFunc<Users, UserRoles> cmpResult = (cmp, U, R) => ( cmp.Property(U.UserName) == "ABC" & cmp.Comparer(U.Password, "=", "111") & cmp.Comparer(R.RoleName, "=", "Role1") ) | ( (cmp.Comparer(U.UserName, "=", "CDE") & cmp.Property(U.Password) == "222" & cmp.Comparer(R.RoleName, "like", "%Role2") ) | (cmp.Property(U.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQL q4 = OQL.From(user) .InnerJoin(roles) .On(user.RoleID, roles.ID) .Select() .Where(cmpResult) .END; Console.WriteLine("OQL by OQLCompareFunc<T1,T2> Test:\r\n{0}", q4); Console.WriteLine(q4.PrintParameterInfo()); q4.Dispose(); }
下面是对应的SQL语句和参数信息:
OQL by OQLCompareFunc<T1,T2> Test: SELECT M.*,T0.* FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE ( M.[UserName] = @P0 AND M.[Password] = @P1 AND T0.[RoleName] = @P2 ) OR ( ( M.[UserName] = @P3 AND M.[Password] = @P4 AND T0.[RoleName] LIKE @P5 ) OR M.[LastLoginTime] > @P6 ) --------OQL Parameters information---------- have 7 parameter,detail: @P0=ABC Type:String @P1=111 Type:String @P2=Role1 Type:String @P3=CDE Type:String @P4=222 Type:String @P5=%Role2 Type:String @P6=2013/7/28 17:31:35 Type:DateTime ------------------End------------------------
3.2.5,委托与闭包
前面我们说道只定义到了3个泛型参数的OQLCompareFun委托,为啥不再继续定义更多参数的泛型委托?
我觉得,这个问题从3方面考虑:
- A,如果你需要连接3个以上的表进行查询,那么你的查询设计过于复杂,可以从数据库或者系统设计上去避免;
- B,泛型具有闭包功能,可以将需要的参数传递进去;
- C,如果定义更多的OQLCompare泛型委托,有可能重蹈“委托之殇”。
如果你不赞成A的说法,查询一定得有3个以上的情况,那么你可以应用B的方式。实际上,该方式前面已经举例过了,再来看一个实际的例子:
void Test3() { Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQLCompareFunc cmpResult = cmp => ( cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111") & cmp.EqualValue(roles.RoleName) ) | ( (cmp.Comparer(user.UserName, OQLCompare.CompareType.Equal, "BCD") & cmp.Property(user.Password) == 222 & cmp.Comparer(roles.ID, "in", new int[] { 1,2,3 }) ) | (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; OQL q3 = OQL.From(user).InnerJoin(roles) .On(user.RoleID, roles.ID) .Select() .Where(cmpResult) .END; Console.WriteLine("OQL by OQLCompareFunc Test:\r\n{0}", q3); Console.WriteLine(q3.PrintParameterInfo()); }
我们在cmpResult 委托的结果中,使用了委托变量之外的参数对象user和roles 。如果有更多的参数委托方法也是可以使用的,这些参数就是委托中的“闭包”,使用该特性,那么再复杂的问题都能够处理了。
再次感叹,委托,真乃神器也!
3.3,Having 之旅—重用之欢
我们再重温一下1.2.4的Having问题,看看那个SQL语句:
SELECT SalesOrderID, SUM(LineTotal) AS SubTotal FROM Sales.SalesOrderDetail GROUP BY SalesOrderID HAVING SUM(LineTotal) > 100000.00 ORDER BY SalesOrderID ;
Having是对分组Group之后的再次筛选,而Where是在Group之前的,所以本质上Having子句也是一个条件表达式,但由于相对Where要简单,我们先用个方法来实现:
public OQL4 Having(object field,object Value,string sqlFunctionFormat) { //具体代码略 }
使用的时候这样用即可:
OQL q5 = OQL.From(user) .Select(user.RoleID).Count(user.RoleID, "count_rolid") .GroupBy(user.RoleID) .Having(user.RoleID, 2,”COUNT{0} > {1} ”)) .END;
上面这种使用方式,首先需要手写Having的聚合函数条件,不是很方便,OQL的Where方法可以使用OQLCompare对象作为比较条件,那么Having也是可以使用的,将Having方法改写下:
public OQL4 Having(OQLCompareFunc cmpFun) { OQLCompare compare = new OQLCompare(this.CurrentOQL); OQLCompare cmpResult = cmpFun(compare); if (!object.Equals(cmpResult, null)) { CurrentOQL.oqlString += "\r\n HAVING " + cmpResult.CompareString; } return new OQL4(CurrentOQL); }
然后OQL中就可以下面这样使用:
OQL q5 = OQL.From(user) .Select(user.RoleID).Count(user.RoleID, "count_rolid") .GroupBy(user.RoleID) .Having(p => p.Count(user.RoleID, OQLCompare.CompareType.GreaterThanOrEqual, 2)) .END; Console.WriteLine("q5:having Test: \r\n{0}", q5); Console.WriteLine(q5.PrintParameterInfo());
程序输出:
q5:having Test: SELECT [RoleID] ,COUNT( [RoleID]) AS count_rolid FROM [LT_Users] GROUP BY [RoleID] HAVING COUNT( [RoleID]) >= @P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=2 Type:Int32 ------------------End------------------------
OQLCompare 成功应用于Having方法,找到问题的类似之处,然后重用问题的解决方案,这不是令人非常欢乐的事情吗:)
四、OQL高级实例
(测试例子说明)
本篇简要介绍了PDF.NET V5 版本的功能增强部分,其它实例请看《OQL实例篇》。
4.1,使用星号查询全部字段
OQL的Select方法如果不传入任何参数,默认将使用关联的实体类的全部字段,使用SelectStar 属性设置“*”进行所有字段的查询,此特性用于某些情况下不想修改实体类但又想将数据库表新增的字段查询到实体类中的情况,比如某些CMS系统,可以让用户自由增加文章表的字段。这些增加或者修改的字段,可以通过entity.PropertyList(“fieldName”) 获取字段的值。
Users user = new Users() { NickName = "pdf.net", RoleID=5 }; OQL q0 = OQL.From(user) .Select() .Where(user.NickName,user.RoleID) .OrderBy(user.ID) .END; q0.SelectStar = true; Console.WriteLine("q0:one table and select all fields \r\n{0}", q0); Console.WriteLine(q0.PrintParameterInfo());
程序输出:
q0:one table and select all fields SELECT * FROM [LT_Users] WHERE [NickName]=@P0 AND [RoleID]=@P1 ORDER BY [ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=5 Type:Int32 ------------------End------------------------
4.2,延迟选取属性字段
有时候可能会根据情况来决定要Select哪些字段,只需要在OQL实例上多次调用Select方法并传入实体类属性参数即可,在最终得到SQL语句的时候才会进行合并处理,实现了延迟选取属性字段的功能,如下面的例子:
OQL q = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END; q.Select(user.LastLoginIP).Where(user.NickName); Console.WriteLine("q1:one table and select some fields\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo());
程序输出:
q1:one table and select some fields SELECT [LastLoginIP], [RoleID], [UserName], [ID] FROM [LT_Users] WHERE [NickName]=@P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=pdf.net Type:String ------------------End------------------------
可以看出,最后输出的是两次Select的结果。
4.3,GroupBy约束
OQL会严格按照SQL的标准,检查在查询使用了GroupBy子句的时候,Select中的字段是否包含在GroupBy子句中,如果不包含,那么会抛出错误结果。某些数据库可能不会有这样严格的约束,从而使得查询结果跟SQL标准的预期不一样,比如SQLite,而在OQL进行这样的约束检查,保证了OQL对于SQL标准的支持,使得系统有更好的移植性。
下面是一个俩联合查询并分组的例子:
OQL q2 = OQL.From(user) .InnerJoin(roles).On(user.RoleID, roles.ID) .Select(user.RoleID, roles.RoleName) .Where(user.NickName, roles.RoleName) .GroupBy(user.RoleID, roles.RoleName) .OrderBy(user.ID) .END; Console.WriteLine("q2:two table query use join\r\n{0}", q2); Console.WriteLine(q2.PrintParameterInfo());
程序输出:
q2:two table query use join SELECT M.[RoleID], T0.[RoleName] FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE M.[NickName]=@P0 AND T0.[RoleName]=@P1 GROUP BY M.[RoleID], T0.[RoleName] ORDER BY M.[ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=role1 Type:String ------------------End------------------------
4.4,多实体Where条件连接查询
SQL中除了多个表之间的左连接、右连接、内连接等Join连接外,还支持一种通过Where条件进行的多表连接的查询,这种查询跟内连接等效。
OQL q3 = OQL.From(user, roles) .Select(user.ID, user.UserName, roles.ID, roles.RoleName) .Where(cmp => cmp.Comparer(user.RoleID, "=", roles.ID) & cmp.EqualValue(roles.RoleName)) .OrderBy(user.ID) .END; Console.WriteLine("q3:two table query not use join\r\n{0}", q3); Console.WriteLine(q3.PrintParameterInfo());
程序输出:
q3:two table query not use join SELECT M.[ID], M.[UserName], T0.[ID], T0.[RoleName] FROM [LT_Users] M ,[LT_UserRoles] T0 WHERE M.[RoleID] = T0.[ID] AND T0.[RoleName] = @P0 ORDER BY M.[ID] --------OQL Parameters information---------- have 1 parameter,detail: @P0=role1 Type:String ------------------End------------------------
4.5,OQLCompare构造函数和操作符重载
下面的例子演示了新版OQL支持的构造函数,需要使用传递OQL参数的重载,同时本例还演示了比较条件的操作符重载,是的代码有更好的可读性。
void Test2() { Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQL q2 = new OQL(user); q2.InnerJoin(roles).On(user.RoleID, roles.ID); OQLCompare cmp = new OQLCompare(q2); OQLCompare cmpResult = ( cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111") & cmp.EqualValue(roles.RoleName) ) | ( (cmp.Comparer(user.UserName, "=", "CDE") & cmp.Property(user.Password) == "222" & cmp.Comparer(roles.RoleName, "like", "%Role2") ) | (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; q2.Select().Where(cmpResult); Console.WriteLine("OQL by OQLCompare Test:\r\n{0}", q2); Console.WriteLine(q2.PrintParameterInfo()); }
程序输出:
OQL by OQLCompare Test: SELECT M.*,T0.* FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE ( M.[UserName] = @P0 AND M.[Password] = @P1 AND T0.[RoleName] = @P2 ) OR ( ( M.[UserName] = @P3 AND M.[Password] = @P4 AND T0.[RoleName] LIKE @P5 ) OR M.[LastLoginTime] > @P6 ) --------OQL Parameters information---------- have 7 parameter,detail: @P0=ABC Type:String @P1=111 Type:String @P2=role1 Type:String @P3=CDE Type:String @P4=222 Type:String @P5=%Role2 Type:String @P6=2013/7/28 22:15:38 Type:DateTime ------------------End------------------------
4.6,OQLCompare委托与泛型委托
参见测试程序的Test3()、Test4()、Test5()方法,原理和部分代码实例已经在3.2 Where迷雾—委托神器 一节中做了详细说明。
4.7,动态构造查询条件
下面的例子演示了如何在OQLCompare委托方法中,动态的根据其它附加条件,构造OQLCompare查询条件,同时也演示了通过Lambda表达式与通过委托方法分别实现动态条件构造的过程,而后者的方式适合在.net2.0 下面编写委托代码。
void TestIfCondition() { Users user = new Users() { ID=1, NickName="abc",UserName="zhagnsan",Password="pwd."}; OQLCompareFunc cmpFun = cmp => { OQLCompare cmpResult = null; if (user.NickName != "") cmpResult = cmp.Property(user.AddTime) > new DateTime(2013, 2, 1); if (user.ID > 0) cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111"); return cmpResult; }; OQL q6 = OQL.From(user).Select().Where(cmpFun).END; Console.WriteLine("OQL by 动态构建 OQLCompare Test(Lambda方式):\r\n{0}", q6); Console.WriteLine(q6.PrintParameterInfo()); } void TestIfCondition2() { Users user = new Users() { ID = 1, NickName = "abc"}; OQL q7 = OQL.From(user) .Select() .Where<Users>(CreateCondition) .END; Console.WriteLine("OQL by 动态构建 OQLCompare Test(委托函数方式):\r\n{0}", q7); Console.WriteLine(q7.PrintParameterInfo()); } OQLCompare CreateCondition(OQLCompare cmp,Users user) { OQLCompare cmpResult = null; if (user.NickName != "") cmpResult = cmp.Property(user.AddTime) > new DateTime(2013, 2, 1); if (user.ID > 0) cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111"); return cmpResult; }
程序输出:
OQL by 动态构建 OQLCompare Test(Lambda方式): SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime] FROM [LT_Users] WHERE [AddTime] > @P0 AND [UserName] = @P1 AND [Password] = @P2 --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/2/1 0:00:00 Type:DateTime @P1=ABC Type:String @P2=111 Type:String ------------------End------------------------
OQL by 动态构建 OQLCompare Test(委托函数方式): SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime] FROM [LT_Users] WHERE [AddTime] > @P0 AND [UserName] = @P1 AND [Password] = @P2 --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/2/1 0:00:00 Type:DateTime @P1=ABC Type:String @P2=111 Type:String ------------------End------------------------
注意:
在 TestIfCondition 方法中,程序中使用了实体类来做if 语句的条件,但是这个实体类是OQL关联的实体类,在使用实体类属性的时候会触发OQL字段堆栈操作。早期版本的PDF.NET SOD框架对此问题支持不是很完善,有可能生成不是预期的SQL语句。该现象在VS的单步调试运行中出现的可能性比较大,这就是以前说的“调试陷阱”。有可能请使用动态查询条件用户,请升级到版本 Ver5.2.3.0429 之后的新版本。
下面再给一个例子:
SalesOrder model = new SalesOrder(); model.iOrderTypeID = "123"; //string orderTypeID = model.iOrderTypeID; BCustomer bCustomer = new BCustomer(); OQLCompareFunc<BCustomer,SalesOrder> cmpFun = (cmp,C,S) => { OQLCompare cmpResult = null; cmpResult = cmp.Comparer(S.iBillID, OQLCompare.CompareType.Equal, 1); if (!string.IsNullOrEmpty(S.iOrderTypeID)) cmpResult = cmpResult & cmp.Comparer(S.iOrderTypeID, OQLCompare.CompareType.Equal, S.iOrderTypeID); int iCityID = 39; //由于调用了关联实体类的 S.iOrderTypeID 用于条件比较,所以下面需要调用 cmp.NewCompare() //cmpResult = cmpResult & cmp.NewCompare().Comparer<int>(C.iCityID, OQLCompare.CompareType.Equal, iCityID); //感谢网友 红枫星空 发现此问题 //或者继续采用下面的写法,但是必须确保 Comparer 方法第一个参数调用为实体类属性,而不是待比较的值 cmpResult = cmpResult & cmp.Comparer(C.iCityID, OQLCompare.CompareType.Equal, iCityID); return cmpResult; }; OQL oQL = OQL.From(model) .LeftJoin(bCustomer).On(model.iCustomerID, bCustomer.ISID) .Select() .Where(cmpFun) .OrderBy(model.iBillID, "desc") .END; Console.WriteLine(oQL); Console.WriteLine(oQL.PrintParameterInfo()); Console.ReadLine();
注意:上面的变量 iCityID 不能等于属性 C.iCityID 的当前值,比如0,这种情况框架无法判断方法使用的实体类属性是在本方法的参数上,还是方法调用前曾经使用过但还没有清理过的实体类属性调用。每当执行了Comparer 方法后,OQL的字段堆栈会清空的,但是这个例子中,它可能没有被清空,从而有可能出错。当然,这里还可以采用 调用 NewCompare 方法的方式,见注释。
正确的输出结果,应该是:
SELECT M.*,T0.* FROM [tb_SalesOrder] M LEFT JOIN [tb_BCustomer] T0 ON M.[iCustomerID] = T0.[ISID] WHERE M.[iBillID] = @P0 AND M.[iOrderTypeID] = @P1 AND T0.[iCityID] = @P2 ORDER BY M.[iBillID] desc --------OQL Parameters information---------- have 3 parameter,detail: @P0=1 Type:Int32 @P1=123 Type:String @P2=39 Type:Int32 ------------------End------------------------
备注:如果需要了解更多的OQL动态条件查询的信息,请参考这篇文章《左求值表达式,堆栈,调试陷阱与ORM查询语言的设计》
4.8,IN 条件子查询
下面的例子使用一个child 的OQL实例作为q的OQL实例的子对象,构造了一 个IN 条件子查询。当前实例演示的是简单子查询,它没有在子查询中引用父查询的字段。
void TestChild() { Users user = new Users(); UserRoles roles = new UserRoles(); OQL child = OQL.From(roles) .Select(roles.ID) .Where(p => p.Comparer(roles.NickName, "like", "%ABC")) .END; OQL q = OQL.From(user) .Select(user.ID,user.UserName) .Where(cmp => cmp.Comparer(user.RoleID, "in", child)) .END; Console.WriteLine("OQL by 子查询Test:\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo()); }
程序输出:
OQL by 子查询Test: SELECT [ID], [UserName] FROM [LT_Users] WHERE [RoleID] IN (SELECT [ID] FROM [LT_UserRoles] WHERE [RoleNickName] LIKE @P0 ) --------OQL Parameters information---------- have 1 parameter,detail: @P0=%ABC Type:String ------------------End------------------------
4.9,高级子查询
高级子查询必须使用OQLChildFunc 委托,并且使用OQL.From(OQL parent,EntityBase entity) 的重载,通过该方式即可在子查询中使用父查询的实体类,而子查询最后作为OQLCompare对象的条件比较方法的参数传入,即下面代码中的
cmp.Comparer(user.RoleID, "=", childFunc)
下面是详细代码:
void TestChild2() { /* SELECT * FROM [LT_Users] WHERE RoleID = (SELECT ID FROM dbo.LT_UserRoles r WHERE [LT_Users].NickName=r.NickName) */ Users user = new Users() { NickName="_nickName"}; UserRoles roles = new UserRoles() { NickName="_roleNickName"}; OQLChildFunc childFunc = parent => OQL.From(parent,roles) .Select(roles.ID) .Where(cmp => cmp.Comparer(user.NickName, "=", roles.NickName) //比较的字段顺序无所谓 & cmp.Property(roles.AddTime) > DateTime.Now.AddDays(-3)) .END; OQL q = OQL.From(user) .Select() .Where(cmp => cmp.Comparer(user.RoleID, "=", childFunc)) .END; q.SelectStar = true; Console.WriteLine("OQL by 高级子查询Test:\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo()); }
程序输出:
OQL by 高级子查询Test: SELECT * FROM [LT_Users] M WHERE [RoleID] = (SELECT [ID] FROM [LT_UserRoles] WHERE M.[NickName] = [RoleNickName] AND [AddTime] > @P0 ) --------OQL Parameters information---------- have 1 parameter,detail: @P0=2013/7/26 22:15:38 Type:DateTime ------------------End------------------------
4.10,批量更新操作
下面的例子使用了Lambda 条件方式的Where作为更新的条件,在被注释的代码中,还演示了旧版本的条件更新方式。如果更新条件对应的数据不是单条的,那么即可实现“批量更新”的效果。
void TestUpdate() { Users user = new Users() { AddTime=DateTime.Now.AddDays(-1), Authority="Read", NickName = "菜鸟" }; OQL q = OQL.From(user) .Update(user.AddTime, user.Authority, user.NickName) .Where(cmp => cmp.Property(user.RoleID) == 100) .END; //OQL q = OQL.From(user) // .Update(user.AddTime) // .Where(user.Authority, user.NickName) // .END; Console.WriteLine("OQL update:\r\n{0}\r\n",q); Console.WriteLine(q.PrintParameterInfo()); }
程序输出:
OQL update: UPDATE [LT_Users] SET [AddTime] = @P0, [Authority] = @P1, [NickName] = @P2 WHERE [RoleID] = @P3 --------OQL Parameters information---------- have 4 parameter,detail: @P0=2013/7/28 22:15:38 Type:DateTime @P1=Read Type:String @P2=菜鸟 Type:String @P3=100 Type:Int32 ------------------End------------------------
4.11,动态排序
有时候我们需要根据用户的选择来决定派系的方式和排序的字段,这个时候就需要查询具有动态排序功能了,只需要在OQL的OrderBy方法内调用一个排序委托方法即可。下面的例子中被注释的部分,总共演示了OQL支持的3种排序方式。
void TestOQLOrder() { Users user = new Users(); //OQLOrderAction<Users> action = this.OQLOrder; OQL q = OQL.From(user) .Select(user.UserName,user.ID) //.OrderBy(p => p.Desc(user.UserName).Asc(user.ID)) //.OrderBy(action,user) .OrderBy<Users>(OQLOrder,user) //3种OQLOrder 对象的使用方法 .END; Console.WriteLine("OQL test OQLOrder object:\r\n{0}\r\n", q); } void OQLOrder(OQLOrder p, Users user) { p.Desc(user.UserName).Asc(user.ID); }
程序输出:
OQL test OQLOrder object: SELECT [UserName], [ID] FROM [LT_Users] ORDER BY [UserName] DESC, [ID] ASC
4.12,批量数据插入
新版本支持通过OQL进行实体类的数据插入,同时还支持高效的直接从数据库的查询结果插入目标表的操作。前者直接使用OQL的Insert方法,后者使用InsertFrom方法。示例只插入了一列数据,如果需要插入多列,在确保子查询返回多列的情况下,用下面的方式:
OQL q = OQL.From(user)
.InsertFrom(child,user.RoleID,user.ID,user.Name,user.NickName);
下面是实际的例子:
void TestInsert() { Users user = new Users() { AddTime = DateTime.Now.AddDays(-1), Authority = "Read", NickName = "菜鸟" }; OQL q = OQL.From(user) .Insert(user.AddTime, user.Authority, user.NickName); Console.WriteLine("OQL insert:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo()); } void TestInsertFrom() { Users user = new Users(); UserRoles roles = new UserRoles(); OQL child = OQL.From(roles) .Select(roles.ID) .Where(cmp => cmp.Comparer(roles.ID, ">", 100)) .END; OQL q = OQL.From(user) .InsertFrom(child,user.RoleID); Console.WriteLine("OQL insert from:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo()); }
程序输出:
OQL insert: INSERT INTO [LT_Users] ( [AddTime], [Authority], [NickName]) VALUES (@P0,@P1,@P2) --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/7/28 22:15:38 Type:DateTime @P1=Read Type:String @P2=菜鸟 Type:String ------------------End------------------------ OQL insert from: INSERT INTO [LT_Users] ( [RoleID] ) SELECT [ID] FROM [LT_UserRoles] WHERE @P0 > [ID] --------OQL Parameters information---------- have 1 parameter,detail: @P0=0 Type:Int32 ------------------End------------------------
4.13,指定查询的锁定方式
SqlServer可以在SQL单条查询语句中指定查询的锁定方式,比如行锁、页锁或者不锁定数据等,详细内容可以参考OQL.SqlServerLock 枚举类型定义,或者参考SqlServer联机帮助。
请注意:如果使用了OQL的With方法指定了查询的锁定方式,那么该条OQL将只能在SqlServer中使用,不利于OQL的跨数据库平台的特性,但由于PDF.NET用户的强烈要求,最终加入了该特性。实际上,对查询的锁定方式,可以通过指定事务的隔离级别实现。
下面的例子实现了对User表查询的 NOLOCK :
void TestSqlLock() { Users user = new Users(); OQL q = OQL.From(user) //.With(OQL.SqlServerLock.NOLOCK) .With("nolock") .Select(user.ID,user.UserName,user.NickName) .END; Console.WriteLine("OQL Test SQL NoLock:\r\n{0}\r\n", q); }
程序输出:
OQL Test SQL NoLock: SELECT [ID], [UserName], [NickName] FROM [LT_Users] WITH(NOLOCK)
4.14,字段的计算条件
不同于2个字段之间的简单比较,有时候可能需要1个字段进行计算后,再跟第2个字段比较,这种条件我们称呼它为“计算条件”。实际上,对1个字段的计算有点类似于对字段使用函数的操作,只是这个“函数”是个匿名函数而已。比如有下面的查询条件:
user.LastLoginTime-user.AddTime>'23:00:00'
比较最后登录时间与用户记录增加的时间要大于23小时(当然这个条件可以通过DateDiff函数来实现,这里只是用它来做一个例子说明计算条件),我们使用“不等式替换”原理,上面的条件可以改写为:
user.LastLoginTime -'23:00:00'>user.AddTime
于是,这个计算条件,就可以使用OQLCompare的“函数条件表达式”了,下面之间给出OQL的例子:
Users user = new Users(); // user.LastLoginTime-user.AddTime>'23:00:00' // => user.LastLoginTime -'23:00:00'>user.AddTime OQL q = OQL.From(user) .Select() .Where(cmp => cmp.Comparer(user.LastLoginTime, ">", user.AddTime, "{0}-'23:00:00'")) .END; q.SelectStar = true; Console.WriteLine("OQL Test SQL Field compute:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序输出:
OQL Test SQL Field compute: SELECT * FROM [LT_Users] WHERE [LastLoginTime]-'23:00:00' > [AddTime] -------No paramter.--------
程序成功达到我们的预期,这说明,只要我们肯思考,问题还是容易解决的。
(注:该小结内容于2013.8.7日增加)
4.15,NOT 逻辑比较条件
SQL的NOT操作用于对条件表达式取反,尽管对于“相等”可以取反得到“不等”操作,但对于比较复杂的组合条件,整体取反从逻辑语义上来说,更容易理解。所以OQL也支持NOT逻辑比较条件。只需要使用OQLCompare.Not() 方法即可,比如有下面的查询语句:
Users user = new Users(); OQL q = OQL.From(user) .Select(user.ID, user.UserName,user.Password) .Where<Users>((cmp, u) => OQLCompare.Not( cmp.Property(u.UserName) == "ABC" & cmp.Property(u.Password) == "123") ) .END; Console.WriteLine("OQL Test NOT Condition:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序输出:
OQL Test NOT Condition: SELECT [ID], [UserName], [Password] FROM [LT_Users] WHERE NOT ( [UserName] = @P0 AND [Password] = @P1 ) --------OQL Parameters information---------- have 2 parameter,detail: @P0=ABC Type:String @P1=123 Type:String ------------------End------------------------
4.16,BETWEEN操作
Between 用于指定条件比较的范围,是包含关系,相当于 min <= x <=max 。用Between可以简化这样的条件比较。
OQLCompare的Between方法是这样定义:
/// <summary> /// 指定条件的包含范围 /// </summary> /// <typeparam name="T">属性字段的类型</typeparam> /// <param name="field">属性字段</param> /// <param name="beginValue">起始值</param> /// <param name="endValue">结束值</param> /// <returns>比较对象</returns> public OQLCompare Between<T>(T field, T beginValue, T endValue) { //略 }
只需要这样使用:
OQL q7 = OQL.From(user).Select() .Where(cmp =>cmp.Between(user.ID,5,10)) .END; q7.SelectStar = true; Console.WriteLine("q7:having Test: \r\n{0}", q7); Console.WriteLine(q7.PrintParameterInfo());
程序输出:
q7:having Test: SELECT * FROM [LT_Users] WHERE [ID] BETWEEN @P0 AND @P1 --------OQL Parameters information---------- have 2 parameter,detail: @P0=5 Type:Int32 @P1=10 Type:Int32 ------------------End------------------------
4.17 使用SQL函数进行比较
有时候,我们可能需要对一个字段进行一个SQL函数计算,然后再让这个结果跟某一个值进行比较,当然这些函数可能在不同的数据库中是不同的,比如SqlServer与Oracle在很多字段处理函数上都不同,下面以SqlServer 求取某个字段的小时数是否大于15点:
Users user = new Users(); OQL q = OQL.From(user) .Select() .Where(cmp => cmp.ComparerSqlFunction(user.LastLoginTime, ">", 15, "DATEPART(hh, {0})")) .END; q.SelectStar = true; Console.WriteLine("OQL Test SQL Fuction:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序输出:
OQL Test SQL Fuction: SELECT * FROM [LT_Users] WHERE DATEPART(hh, [LastLoginTime]) > @P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=15 Type:Int32 ------------------End------------------------
通过这种方式,我们能够在比较条件上应用任何SQL函数,相比EF,这种方式要简单。注意这里使用的是 OQLCompare的 ComparerSqlFunction 函数。
附录
下面是PDF.NET Ver5.0版本OQL测试完整的源码和SQL输出,其中大部分内容已经在上面的章节中做过说明,但有少部分未在正文中做说明,供大家集中参考。
附录相关的程序将在PDF.NET的开源项目 http://pwmis.codeplex.com 下载页面提供下载, PDF.NET_V5.0_Beta_20130807 (已经更新,之前下载过的请重新下载)
有关框架更多的信息,请参考框架官网 http://www.pwmis.com/sqlmap 。
附录1:OQL测试完整源码
附录2:OQL测试程序输出的SQL信息