1,表达式的求值顺序与堆栈结构

“表达式” 是程序语言一个很重要的术语,也是大家天天写的程序中很常见的东西,但是表达式的求值顺序一定是从左到右么? C/C++语言中没有明确规定表达式的运算顺序(从左到右,或是从右到左),这点与C#及Java语言都不同。不过可以确定的是,C#表达式的求值顺序一定是从左到右的。这个问题虽然对于大多数情况来说不重要,甚至很多普通C#,Java开发者都会忽略的问题,但是对于语言设计者,框架设计者,这是有可能需要考虑的问题。

堆栈是2种数据结构,“栈” 是一种后进先出的数据结构,也就是说后存放的先取,先存放的后取。这就如同我们要取出放在箱子里面底下的东西,我们首先要移开压在它上面的物体。这个特点常用于函数的嵌套调用,用于记录每一次函数调用的点,以便下级函数调用完毕后返回该记录点继续执行,最典型的应用就是函数的递归调用。

根据表达式的求值顺序,再结合堆栈结构,程序语言就可以知道表达式的调用结构,知道方法参数的求值顺序,SOD框架恰好利用了这个特征来构建ORM查询语言--OQL

2,“字段堆栈”与实体类属性调用的“秘密”

OQL内置了一个堆栈对象:

  1. /// <summary>
  2. /// 字段堆栈
  3. /// </summary>
  4. protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();

这个堆栈内存放的是表名称字段对象,它的定义是:

  1. public class TableNameField
  2. {
  3. /// <summary>
  4. /// 获取表名称
  5. /// </summary>
  6. public string Name { get;}
  7. /// <summary>
  8. /// 原始字段名
  9. /// </summary>
  10. public string Field;
  11. /// <summary>
  12. /// 关联的实体类
  13. /// </summary>
  14. public EntityBase Entity;
  15. /// <summary>
  16. /// 在一系列字段使用中的索引号
  17. /// </summary>
  18. public int Index;
  19. /// <summary>
  20. /// 字段对应的值
  21. /// </summary>
  22. public object FieldValue;
  23.  
  24. /// <summary>
  25. /// 在SQL语句中使用的字段名
  26. /// </summary>
  27. public string SqlFieldName
  28. {
  29. get;set;
  30. }
  31. }

TableNameField

在每一个OQL对象上,都有关联的SOD框架的实体类,它有一个“属性访问事件”,OQL对象订阅了该事件:

  1. public class OQL
  2. {
  3. /// <summary>
  4. /// 字段堆栈
  5. /// </summary>
  6. protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();
  7.  
  8. public OQL(EntityBase e)
  9. {
  10. //其它略
  11. e.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(e_PropertyGetting);
  12. }
  13.  
  14. void e_PropertyGetting(object sender, PropertyGettingEventArgs e)
  15. {
  16. TableNameField tnf = new TableNameField()
  17. {
  18. Field = e.PropertyName,
  19. Entity = (EntityBase)sender,
  20. Index = this.GetFieldGettingIndex()
  21. };
  22.  
  23. fieldStack.Push(tnf);
  24. }
  25.  
  26. //其它方法略
  27. }

这样,在OQL实例表达式中,每一次调用关联的实体类的属性,就会将该属性对应的字段名信息,压入字段堆栈。这些字段信息,将用来构造SQL的 Select,Where,Order 子句,本篇将讲解它是如何构造Where条件子句的。

OQL的Where方法支持多种条件构造方式,其中一种是使用OQLCompare对象来做条件。由于OQLCompare 对象设计成了OQL的子对象,因此它也能访问 fieldStack 对象,利用它提供的信息,构造条件信息。

  1. /// <summary>
  2. /// 实体对象条件比较类,用于复杂条件比较表达式
  3. /// </summary>
  4. public class OQLCompare //: IDisposable
  5. {
  6. /// <summary>
  7. /// 关联的OQL对象
  8. /// </summary>
  9. public OQL LinkedOQL { get;protected internal set; }
  10.  
  11. public OQLCompare(OQL oql)
  12. {
  13. if (oql == null)
  14. throw new ArgumentException("OQLCompare 关联的OQL对象为空!");
  15. this.LinkedOQL = oql;
  16. }
  17.  
  18. //其它内容略
  19. }

此后,就可以像下面这样构造并使用一个OQL查询对象:

  1. User user=new User();
  2. OQL q=OQL.From(user)
  3. .Select(user.ID,user.Name)
  4. .Where(cmp=>cmp.Comparer(user.Age,">",))
  5. .END;
  6. List<User> users=EntityQuery<User>.QueryList(q);

这个OQL查询是在查询所有年龄大于18岁的用户,在Where方法中,cmp对象就是一个OQLCompare 对象,它的Comparer方法使用了user对象的Age属性,在方法执行的时候,user.Age 被求值,字段名“Age” 被压入OQL的字段堆栈,

  1. Stack:(--“Age”)

于是,OQL可以构造出类似下面的SQL语句:

  1. Select ID ,Name From Tb_User
  2. Where Age > @P0
  3. -- P0 = 18

当然我们可以直接调用OQL的方法,打印出SQL语句和参数信息,下面会说。

聪明的读者你可能想到了,这是在利用表达式求值得“副作用”啊,本来只是对 user.Age 属性求值而已,但却利用该属性求值过程中引发的事件,得到了使用的字段信息,然后利用这个信息来构造SQL语句!

这是一个“巧妙”的运用,OQL避开了反射,也没有使用"表达式树",所以OQL生成SQL的过程非常高效,不会有EF的第一次查询非常慢的问题。

在OQLCompare对象的Comparer方法上,第三个参数除了是一个要比较的值,也可以是另外一个字段,例如下面的查询规则定义的符合最低年龄设置的用户:

  1. User user=new User();
  2. Rule rule = new Rule();
  3. OQL q=OQL.From(user)
  4. .InnerJoin(rule).On(user.RuleID,rule.ID)
  5. .Select(user.ID,user.Name)
  6. .Where(cmp=>cmp.Comparer(user.Age,">",rule.LowAge))
  7. .END;
  8. List<User> users=EntityQuery<User>.QueryList(q);

该查询会生成下面的SQL语句:

  1. Select M.ID,M.Name
  2. From Tb_User M
  3. Inner Join Tb_Rule T0 ON M.RuleID = T0.ID
  4. Where M.Age > T0.LowAge

在这个查询中,OQLCompare对象使用的OQL字段堆栈的情况是:

  1. 调用方法 Comparer
  2. 求取 uer.Age属性,得到 "M.Age" 字段名,压入字段堆栈;
  3. 求取 rule.LowAg属性, 得到 "T0.LowAge" 字段名,压入字段堆栈;

假设此时程序运行在调试状态,在这里有一个断点中断了,在VS的IDE 上查看了其它属性的值,比如看了下 user.ID,user.Name,那么此时OQL的堆栈数据是:

  1. Stack:(--“M.ID”,--“M.Name”)

当方法Comparer 执行后,堆栈的结果是:

  1. Stack:(--“T0.LowAge”,--“M.Age”, --“M.ID”,--“M.Name”)

调用OQL方法,生成条件字符串的时候,从该堆栈弹出字段信息:

  1. Pop Stack:--“T0.LowAge
  2.  
  3. Pop Stack:--“M.Age

实际上,在OQLComare对象的Comparer方法中进行了上面的堆栈“弹出”操作,并且返回了一个新的 OQLCompare 对象,根据C#语言的“左求值表达式”原则 ,这个新的OQLCompare 对象获得了下面的信息:

  1. compare.ComparedFieldName ="M.Age" ;
  2. compare.ComparedParameterName ="T0.LowAge" ;
  3. compare.ComparedType =">" ;

该信息完全表达了构建OQL查询的“原意“,并指导生成正确的查询条件:

  1. M.Age > T0.LowAge

由于每次调用Comparer方法都生成了这样的一个新的 OQLCompare 对象,所以整个OQLCompare 对象是一个“组合对象”,组合中有根,有枝条,有叶子,组合成为一个“条件对象树”,有这样一棵树,那么再复杂的查询条件,都可以表示了。

3,动态构造查询条件与“调试陷阱”

从上面的举例,我们发现OQLCompare对象即能够进行【字段与值】进行比较,又能够进行【字段与字段】的条件比较,而且也能识别不同表的字段在一起进行比较。

但是,在这个过程中,有可能遭遇”调试陷阱“。

3.1,字段堆栈--避免“调试陷阱”

回看开始的例子:

  1. User user=new User();
  2. OQL q=OQL.From(user)
  3. .Select(user.ID,user.Name)
  4. .Where(cmp=>cmp.Comparer(user.Age,">",))
  5. .END;
  6. List<User> users=EntityQuery<User>.QueryList(q);

加入有色背景处是一个断点,程序运行到这里进入调试模式,而此时鼠标放在了 user.ID上面,那么当方法执行到 Comparer里面去以后,我们来看看堆栈的结果:

  1. Stack:(--“Age”,--“ID”)

在方法执行过程中,首先弹出第一个值:

  1. Pop Stack:--“Age

但是SOD框架并不知道这个字段信息是 Comparer方法的第一个参数,还是第三个参数,不过拿 user.Age 的值跟第三个参数的值 18 进行比较,user.Age !=18 ,所以可以断定,字段信息”Age“ 发生在方法的第一个参数调用上,而不是第三个参数,因此,字段堆栈的第二个元素,(1-- ”ID“) 也就没有必要弹出了,等到方法执行完成,将Stack 字段堆栈清除即可,这样在下一次调用开始的时候,不会造成干扰。

所以这里的情况是在调试的时候,给字段堆栈增加了新的元素,如果此时 user.Age==18 ,那么 cmp.Comparer(user.Age,">",18) 不会生成预期的SQL,从而产生”调试陷阱“。产生这个问题的具体原因,请看下面的内容。

当然,当前小节这个OQL查询在非调试状态下运行是没有问题的,字段堆栈的执行原理可以避免”调试陷阱“的问题。

3.2,动态构造查询条件的 类“调试陷阱”

上面的字段堆栈处理方案并不能完全化解”调试陷阱“的问题,而且,有时候这个问题不是发生在调试状态,也有可能发生在动态构造条件的过程中,请参考下面的例子:

  1. void TestIfCondition2()
  2. {
  3. Users user = new Users() { ID = , NickName = "abc", UserName="zhang san", Password="pwd111" };
  4. OQL q7 = OQL.From(user)
  5. .Select()
  6. .Where<Users>(CreateCondition)
  7. .END;
  8. Console.WriteLine("OQL by 动态构建 OQLCompare Test(委托函数方式):\r\n{0}", q7);
  9. Console.WriteLine(q7.PrintParameterInfo());
  10. }
  11.  
  12. OQLCompare CreateCondition(OQLCompare cmp, Users user)
  13. {
  14. OQLCompare cmpResult = null;
  15. if (user.NickName != "")
  16. cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
  17. // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
  18. if (user.ID > )
  19. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  20. else
  21. cmpResult = cmpResult & cmp.Comparer(user.UserName, "=", "zhang san")
  22. & cmp.Comparer(user.Password, "=", "pwd111");
  23. return cmpResult;
  24. }

运行这个程序,会输出下面的SQL语句和参数信息:

  1. OQL by 动态构建 OQLCompare Test(委托函数方式):
  2. SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],
  3. [LastLoginTime],[LastLoginIP],[Remarks],[AddTime]
  4. FROM [LT_Users]
  5. WHERE [NickName] = @P0 AND [ID] = [UserName] AND [Password] = @P1
  6. --------OQL Parameters information----------
  7. have 2 parameter,detail:
  8. @P0=abc Type:String
  9. @P1=pwd111 Type:String
  10. ------------------End------------------------

请注意SQL条件中的背景标注部分,[ID] =  [UserName] 这个条件,显然不是我们期望的,出现这个问题的原因是什么呢?
原来问题出在这个程序段:

  1. if (user.ID > 0)
  2. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  3. else
  4. cmpResult = cmpResult & cmp.Comparer(user.UserName, "=", "zhang san")
  5. & cmp.Comparer(user.Password, "=", "pwd111");

程序选择了 else 分支,执行了cmp.Comparer(user.UserName, "=", "zhang san")  这句,但是,在本例中,user.UserName 的值恰好就是 “zhang san”,所以 Comparer方法的第一个参数和第三个参数的值是一样的,而此时的OQL堆栈的数据是:

  1. Stack:(--“UserName”,--“ID”)

OQL会首先弹出堆栈的元素 "UserName" 字段,然后让它对应的实体类属性值与Comparer方法的第三个参数值进行比较,发现这2个值是相同的,于是假设"UserName"字段调用发生在Comparer方法的第三个参数上,于是继续弹出OQL字段堆栈的下一个元素:

  1. Pop Stack:1--“ID

于是将字段名“ID” 作为Comparer方法的第一个参数调用的“副作用”结果,构造成了 [ID] =  [UserName] 这个条件。

这个错误出现的情况并不常见,简单说就是只有完全且同时符合以下的情况,才会产生问题:

  1. 当Comparer方法执行前,调用过OQL关联的实体类的属性(既属性求值),(如果最近的一次实体类属性调用发生在OQLCompare对象的某个方法内则不符合本条件)
  2. 且方法的第一个参数和第三个参数的值一样的时候,
  3. 第三个参数不是一个实体类属性调用,而是一个单纯变量或者值

3.3,消除复杂查询条件的“字段堆栈“干扰

要解决这个问题也很容易,将上面的代码改写成下面这个样子:

  1. OQLCompare CreateCondition(OQLCompare cmp, Users user)
  2. {
  3. OQLCompare cmpResult = null;
  4. if (user.NickName != "")
  5. cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
  6. // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
  7. if (user.ID > )
  8. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  9. else
  10. cmpResult = cmpResult & cmp.EqualValue(user.UserName)
  11. & cmp.Comparer(user.Password, "=", "pwd111");
  12. return cmpResult;
  13. }

这里将使用 user.UserName 自身的值进行相等比较,避免了字段堆栈的影响。如果不是自身的值相等比较,那么还可以利用操作符重载,进行更多的比较方式,比如大于,小于等:

  1. OQLCompare CreateCondition(OQLCompare cmp, Users user)
  2. {
  3. OQLCompare cmpResult = null;
  4. if (user.NickName != "")
  5. cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
  6. // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
  7. if (user.ID > )
  8. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  9. else
  10. cmpResult = cmpResult & cmp.Property(user.UserName) == "zhang san"
  11. & cmp.Comparer(user.Password, "=", "pwd111");
  12. return cmpResult;
  13. }

如果出于性能上的考虑或者进行Like 查询等,必须使用Comparer 方法,要解决这种“属性与比较的值相等”的OQL堆栈字段干扰问题,还可调用OQLCompare对象的的NewCompare方法:

  1. OQLCompare CreateCondition(OQLCompare cmp, Users user)
  2. {
  3. OQLCompare cmpResult = null;
  4. if (user.NickName != "")
  5. cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
  6. // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
  7. if (user.ID > )
  8. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  9. else
  10. cmpResult = cmpResult & cmp.NewCompare().Comparer(user.UserName,"=", "zhang san")
  11. & cmp.Comparer(user.Password, "=", "pwd111");
  12. return cmpResult;
  13. }

如果觉得上面的方式繁琐,那么还有一个更直接的办法,就是动态构造条件的时候,不在关联的实体类上调用属性进行条件判断,而是创建另外一个实体类对象(不可以使用克隆的方式):

  1. OQLCompare CreateCondition(OQLCompare cmp, Users user)
  2. {
  3. Users testUser = new Users { NickName =user.NickName , ID =user.ID};
  4.  
  5. OQLCompare cmpResult = null;
  6. if (testUser.NickName != "")
  7. cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
  8. // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
  9. if (testUser.ID > )
  10. cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
  11. else
  12. cmpResult = cmpResult & cmp.Comparer(user.UserName,"=", "zhang san")
  13. & cmp.Comparer(user.Password, "=", "pwd111");
  14. return cmpResult;
  15. }

当然,可能最简单的方式,还是你有意让Comparer 方法的第一实体类属性值参数和第三个普通值参数的值不要相等,这在大多数情况下都是可以做到的。

采用上面的方式处理后,对于OQL动态构造查询条件,可以得到下面正确的SQL信息:

  1. OQL by 动态构建 OQLCompare Test(委托函数方式):
  2. SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],
  3. [LastLoginTime],[LastLoginIP],[Remarks],[AddTime]
  4. FROM [LT_Users]
  5. WHERE [NickName] = @P0 AND [UserName] = @P1 AND [Password] = @P2
  6. --------OQL Parameters information----------
  7. have 3 parameter,detail:
  8. @P0=abc Type:String
  9. @P1=zhang san Type:String
  10. @P2=pwd111 Type:String
  11. ------------------End------------------------

小节

本篇说明了编程语言左求值表达式规则,堆栈数据结构,并利用这两个特征,结合属性调用事件 ,巧妙的设计了SOD框架的”ORM查询语言“--OQL,并详细的分析了可能产生的问题与解决方案。如果使用PDF.NET SOD框架来处理动态的查询条件,那么本篇文章一定要仔细阅读一下。

感谢大家一直以来对于PDF.NET SOD框架的支持,

框架官网地址:http://www.pwmis.com/sqlmap

开源项目地址:http://pwms.codeplex.com

注意:本文的解决方案和实例程序,需要SOD框架的新版本 5.2.3.0429 以上支持,如果程序中有动态构造查询条件的情况,请大家及时获取最新的源代码。

左求值表达式,堆栈,调试陷阱与ORM查询语言的设计的更多相关文章

  1. python 求值表达式解析

    采用中缀转后缀的算法. 注意我输入的格式. #注意格式 def suffix(st): listopt=[" "] listnum=[" "] for i in ...

  2. Excel求值表达式——太好用了

    这个需要通过宏表函数EVALUATE来实现,首先需要自定义名称.如果数据在A列,那么在B列自定义名称,按Ctrl+F3键,在“定义名称框”中选择“新建”,然后输入名称为“结果”,数据来源输入=EVAL ...

  3. 求值器本质--eval&apply

    最近跟着(How to Write a (Lisp) Interpreter (in Python))使用python实现了一个简易的scheme解释器.不得不说使用python这类动态语言实现不要太 ...

  4. Scala - 快速学习04 - 求值策略

    表达式求值策略(Evaluation Strategy) Scala中所有的运算都是基于表达式的. Call By Value - 对函数实参求值,且仅求值一次:函数调用之前对所有表达式进行求值 Ca ...

  5. PTA笔记 堆栈模拟队列+求前缀表达式的值

    基础实验 3-2.5 堆栈模拟队列 (25 分) 设已知有两个堆栈S1和S2,请用这两个堆栈模拟出一个队列Q. 所谓用堆栈模拟队列,实际上就是通过调用堆栈的下列操作函数: int IsFull(Sta ...

  6. C++之字符串表达式求值

    关于字符串表达式求值,应该是程序猿们机试或者面试时候常见问题之一,昨天参加国内某IT的机试,压轴便为此题,今天抽空对其进行了研究. 算术表达式中最常见的表示法形式有 中缀.前缀和 后缀表示法.中缀表示 ...

  7. C语言中缀表达式求值(综合)

    题前需要了解的:中缀.后缀表达式是什么?(不知道你们知不知道,反正我当时不知道,搜的百度) 基本思路:先把输入的中缀表达式→后缀表达式→进行计算得出结果 栈:"先进先出,先进后出" ...

  8. Java 计算数学表达式(字符串解析求值工具)

    Java字符串转换成算术表达式计算并输出结果,通过这个工具可以直接对字符串形式的算术表达式进行运算,并且使用非常简单. 这个工具中包含两个类 Calculator 和 ArithHelper Calc ...

  9. 用Python3实现表达式求值

    一.题目描述 请用 python3 编写一个计算器的控制台程序,支持加减乘除.乘方.括号.小数点,运算符优先级为括号>乘方>乘除>加减,同级别运算按照从左向右的顺序计算. 二.输入描 ...

随机推荐

  1. 封装WebAPI客户端,附赠Nuget打包上传VS拓展工具

    一.前言 上篇< WebAPI使用多个xml文件生成帮助文档 >有提到为什么会出现基于多个xml文件生成帮助文档的解决方案,因为定义的模型可能的用处有: 1:单元测试 2:其他项目引用(可 ...

  2. ScheduleThreadPoolExecutor的工作原理与使用示例

    欢迎探讨,如有错误敬请指正 如需转载,请注明出处 http://www.cnblogs.com/nullzx/ 1. ScheduleExecutorService接口.ScheduledFuture ...

  3. Design3:使用HierarchyID构建数据的分层结构

    1,传统的分层结构是父子结构,表结构中有一个ParentID字段自引用表的主键,表示“归属”关系,例如 create table dbo.emph ( ID int not null primary ...

  4. vc操作windows防火墙的方法

    收藏该地址,以备不时之需. http://msdn.microsoft.com/en-us/library/aa364726.aspx

  5. .NET面试题解析(01)-值类型与引用类型

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 常见面试题目: 1. 值类型和引用类型的区别? 2. 结构和类的区别? 3. delegate是引用类型还 ...

  6. 【原创】C#玩高频数字彩快3的一点体会

    购彩风险非常高,本人纯属很久以前对数字高频彩的一点研究.目前已经远离数字彩,重点研究足球篮球比赛资料库和赛果预测. 这是一篇在草稿箱保存了1年多的文章,一直没发现,顺便修改修改分享给大家.以后会有更多 ...

  7. 关于有默认值的字段在用EF做插入操作时的思考

    今天在用EF做插入操作的时候发现数据库中一个datetime类型的字段(CreateDate)的值居然全部为null.于是赶紧看表结构发现CreateDate字段居然是允许为空的. 虽然为空,但是设置 ...

  8. 用DropDownList实现的省市级三级联动

    这是一个用DropDownList 实现的省市级三级联动,记录一下········ <asp:ScriptManager ID="ScriptManager1" runat= ...

  9. 微信小程序中rpx与rem单位使用

    原作者: 小小小   来自: 授权地址 本文讲解rpx和rem应用于微信小程序,如果你还没有入门,建议先从下面看起: 微信小程序官方文档web app变革之remrpx单位官方文档rpx单位基础介绍 ...

  10. Java多线程读取大文件

    前言 今天是五一假期第一天,按理应该是快乐玩耍的日子,但是作为一个北漂到京师的开发人员,实在难想出去那玩耍.好玩的地方比较远,近处又感觉没意思.于是乎,闲着写篇文章,总结下昨天写的程序吧. 昨天下午朋 ...