zoukankan      html  css  js  c++  java
  • 左求值表达式,堆栈,调试陷阱与ORM查询语言的设计

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

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

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

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

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

    OQL内置了一个堆栈对象:

     /// <summary>
    /// 字段堆栈
    /// </summary>
    protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();

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

     public class TableNameField
        {
            /// <summary>
            /// 获取表名称
            /// </summary>
            public string Name {  get;}
            /// <summary>
            /// 原始字段名
            /// </summary>
            public string Field;
            /// <summary>
            /// 关联的实体类
            /// </summary>
            public EntityBase Entity;
            /// <summary>
            /// 在一系列字段使用中的索引号
            /// </summary>
            public int Index;
            /// <summary>
            /// 字段对应的值
            /// </summary>
            public object FieldValue;
          
            /// <summary>
            /// 在SQL语句中使用的字段名
            /// </summary>
            public string SqlFieldName
            {
               get;set;
            }
        }
    TableNameField

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

    public class OQL
    {
            /// <summary>
            /// 字段堆栈
            /// </summary>
            protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();
    
            public OQL(EntityBase e)
            {
                //其它略
                e.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(e_PropertyGetting);
            }
    
            void e_PropertyGetting(object sender, PropertyGettingEventArgs e)
            {
                TableNameField tnf = new TableNameField()
                {
                    Field = e.PropertyName,
                    Entity = (EntityBase)sender,
                    Index = this.GetFieldGettingIndex()
                };
             
                fieldStack.Push(tnf);
            }
    
            //其它方法略
    }

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

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

        /// <summary>
        /// 实体对象条件比较类,用于复杂条件比较表达式
        /// </summary>
        public class OQLCompare //: IDisposable
        {
           /// <summary>
            /// 关联的OQL对象
            /// </summary>
            public OQL LinkedOQL { get;protected internal set; }
    
            public OQLCompare(OQL oql)
            {
                if (oql == null)
                    throw new ArgumentException("OQLCompare 关联的OQL对象为空!");
                this.LinkedOQL = oql;
            }
    
           //其它内容略
       }


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

    User user=new User();
    OQL q=OQL.From(user)
      .Select(user.ID,user.Name)
      .Where(cmp=>cmp.Comparer(user.Age,">",18))
    .END;
    List<User> users=EntityQuery<User>.QueryList(q);

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

    Stack:(0--“Age”)

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

    Select ID ,Name From Tb_User
      Where Age > @P0
    -- P0 = 18

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

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

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

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

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

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

    Select M.ID,M.Name
      From Tb_User M
        Inner Join Tb_Rule T0 ON M.RuleID = T0.ID 
        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的堆栈数据是:

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

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

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

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

    Pop Stack:0--“T0.LowAge”
    
    Pop Stack:1--“M.Age”

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

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

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

    M.Age > T0.LowAge

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

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

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

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

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

    回看开始的例子:

    User user=new User();
    OQL q=OQL.From(user)
      .Select(user.ID,user.Name)
      .Where(cmp=>cmp.Comparer(user.Age,">",18))
    .END;
    List<User> users=EntityQuery<User>.QueryList(q);

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

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

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

    Pop Stack:0--“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,动态构造查询条件的 类“调试陷阱”

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

            void TestIfCondition2()
            {
                Users user = new Users() { ID = 0, NickName = "abc", UserName="zhang san", Password="pwd111" };
                OQL q7 = OQL.From(user)
                    .Select()
                    .Where<Users>(CreateCondition)
                    .END;
                Console.WriteLine("OQL by 动态构建 OQLCompare Test(委托函数方式):
    {0}", q7);
                Console.WriteLine(q7.PrintParameterInfo());
            }
    
            OQLCompare CreateCondition(OQLCompare cmp, Users user)
            {
                OQLCompare cmpResult = null;
                if (user.NickName != "")
                    cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
                // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
                if (user.ID > 0)
                    cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
                else
                    cmpResult = cmpResult & cmp.Comparer(user.UserName, "=", "zhang san")  
                                          & cmp.Comparer(user.Password, "=", "pwd111");
                return cmpResult;
            }

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

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

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

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

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

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

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

    Pop Stack:1--“ID”

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

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

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

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

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

     OQLCompare CreateCondition(OQLCompare cmp, Users user)
     {
         OQLCompare cmpResult = null;
         if (user.NickName != "")
             cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
         // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
         if (user.ID > 0)
             cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
         else
             cmpResult = cmpResult & cmp.EqualValue(user.UserName)  
                                   & cmp.Comparer(user.Password, "=", "pwd111");
         return cmpResult;
     }

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

     OQLCompare CreateCondition(OQLCompare cmp, Users user)
     {
         OQLCompare cmpResult = null;
         if (user.NickName != "")
             cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
         // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
         if (user.ID > 0)
             cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
         else
             cmpResult = cmpResult & cmp.Property(user.UserName) == "zhang san" 
                                   & cmp.Comparer(user.Password, "=", "pwd111");
         return cmpResult;
     }

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

     OQLCompare CreateCondition(OQLCompare cmp, Users user)
     {
         OQLCompare cmpResult = null;
         if (user.NickName != "")
             cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
         // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
         if (user.ID > 0)
             cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
         else
             cmpResult = cmpResult & cmp.NewCompare().Comparer(user.UserName,"=", "zhang san") 
                                   & cmp.Comparer(user.Password, "=", "pwd111");
         return cmpResult;
     }

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

     OQLCompare CreateCondition(OQLCompare cmp, Users user)
     {
         Users testUser = new Users {  NickName =user.NickName , ID =user.ID};
    
         OQLCompare cmpResult = null;
         if (testUser.NickName != "")
             cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
         // 上面一行,也可以采用这样的写法: cmpResult = cmp.EqualValue(user.NickName);
         if (testUser.ID > 0)
             cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
         else
             cmpResult = cmpResult & cmp.Comparer(user.UserName,"=", "zhang san") 
                                   & cmp.Comparer(user.Password, "=", "pwd111");
         return cmpResult;
     }

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

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

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

     小节

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

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

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

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

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

  • 相关阅读:
    85. Maximal Rectangle
    120. Triangle
    72. Edit Distance
    39. Combination Sum
    44. Wildcard Matching
    138. Copy List with Random Pointer
    91. Decode Ways
    142. Linked List Cycle II
    异或的性质及应用
    64. Minimum Path Sum
  • 原文地址:https://www.cnblogs.com/bluedoctor/p/4470526.html
Copyright © 2011-2022 走看看