【手擼一個ORM】第五步、Expression(表達式目錄樹)轉換為Where子句


說明

在SQL中,查詢、修改比較常用到WHERE子句,在這里根據使用場景不同,定義了兩個類,一個用於查詢,一個用於修改(插入)操作。原因是:

  • 查詢操作支持一級導航屬性查詢,如student.School.Name="xxx",在這里使用LEFT JOIN 的方式實現,所以拼接的時候需要考慮子表別名(兩個表有相同字段,所以必須通過表名前綴進行區分)等問題。
  • 更新操作不支持導航屬性,如 student.School.Name="xxx" 將會被忽略。說明一點:這里也有簡單的方法可以實現一級導航屬性的查詢,方法是將其解釋為子查詢 如 Update Student Set xxx WHERE SchoolId IN (SELECT Id FROM School WHERE Name='xxx')。這個擴展應該比較簡單,請自行實現。

 一些思考:

需要實現參數查詢,這是基本要求,雖然使用其他方式也能屏蔽SQL注入,但參數化無疑是更好的選擇。

下面是嘗試過的實現方式:

  • 繼承ExpressionVisitor,在遍歷節點時拼接查詢條件。如果表達式中完全是二元操作,如 s => s.Name=="張三" && s.IsDel=false,這種方法可以實現的很好,但是如果有如下情況 s => !s.IsDel,那問題就來了,我們需要的是 [Student].[IsDel]=0,而這種方式只能解析出 [Student].[IsDel],類似的情形還有不少,所以只能放棄。
  • 繼承ExpressionVisitor這種方式嘗試失敗,得到的教訓是必須將二元表達式中 AndAlso、OrElse與其他表達式區分出來,將AND和OR區間的子句摘出來解析后再用AND | OR連接起來,這個也簡單,遍歷AND | OR,將子句封裝起來保存到列表,大概是這樣的:new ClauseHolder { Type="And", Expr = 表達式 },最后再將封裝好的列表解釋成SQL語句,但這種方式也有缺陷,就是當表達式樹有嵌套的情況,簡單的List<ClauseHolder>無法體現,如果非要添加嵌套,邏輯比較復雜,所以最后也放棄了。
  • 既然上面兩種解決方式都有缺陷,那就嘗試將他們結合起來,最終的方案是將And|Or的子句摘出來單獨解析(第二種方案),將解析過程中直接拼裝字符串(第一種方案)。

思路分析

將單一條件作為一個子句 如SQL語句時這樣的:Student.Id>0 AND Student.IsDel=0,其中 Student.Id > 0 是一個子句, Student.IsDel = 0 是一個子句。

整體的思路就是,從右到左,將子句轉換為SQL語句,並使用 AND | OR 連接起來。我們知道,表達式目錄樹是二叉樹的結構,從右到左一級級進入,對AND|Or左側表達式的解析要使用遞歸操作。

分析到這里,我們似乎找到了一點點頭緒,拋開其他,單一子句的解析先整出來,下面是我想到的幾種情形:

表達式目錄樹解析的最大難點就是可能出現的情況太多,每種情況對應的節點類型、操作方式可能都不一樣。我暫時能想到的就這么多,解決的思路也在上面。其中用的最多的,就是判斷根節點類型和節點取值操作,還好,在上一篇Expression擴展中我們已經封裝了響應的方法,直接調用即可。

當然,在實際代碼中,我們還加入了一些邏輯,如導航屬性的處理,請看下面的表達式

Expression<Func<Student, bool>> expr1 = s => s.School.IsDel;

這里判斷是的Student類的導航屬性School的IsDel屬性,我們的查詢時支持這種只有一級的導航屬性的,所以對這種情況進行了一些處理。另外可能數據表列名與屬性名並不相同,在拼接的時候,不得不從實體緩存中找到相應的屬性定義,將其轉換為對應的數據列。

接下來,需要應對的將組合條件拼接起來的問題,並不復雜,這里就不再多做解釋,直接看代碼就可以了。

條件表達式解釋器基類 [BaseConditionResolver.cs]

using Dapper;
using System.Collections.Generic;
using MyOrm.DbParameters;
using MyOrm.Reflections;

namespace MyOrm.Expressions
{
    public class QueryConditionResolveResult
    {
        public string Condition { get; set; }

        public MyDbParameters Parameters { get; set; } = new MyDbParameters();

        public List<MyEntity> NavPropertyList { get; set; } = new List<MyEntity>();
    }

    public abstract class BaseConditionResolver<T>
    {
        #region 字段

        protected MyEntity Entity { get; }

        private readonly string _prefix;

        private readonly Stack<string> _stack = new Stack<string>();

        protected readonly QueryConditionResolveResult Result = new QueryConditionResolveResult();

        private int _parameterIndex;

        #endregion

        #region 構造函數

        protected BaseConditionResolver(string prefix = "@")
        {
            Entity = MyEntityContainer.Get(typeof(T));
            _prefix = prefix;
        }

        protected BaseConditionResolver(MyEntity entity, string prefix = "@")
        {
            Entity = entity;
            _prefix = prefix;
        }

        #endregion

        #region 返回結果

        public QueryConditionResolveResult Resolve(Expression expression)
        {
            Visit(expression);
            var condition = string.Concat(_stack.ToArray());
            Result.Condition = condition;
            _stack.Clear();
            return Result;
        }
        #endregion

        #region 處理表達式目錄樹

        private void Visit(Expression node)
        {
            if (node.NodeType == ExpressionType.AndAlso ||
                node.NodeType == ExpressionType.OrElse)
            {
                var expression = (BinaryExpression)node;
                var right = expression.Right;
                var left = expression.Left;

                var rightString = ResolveExpression(right);
                var op = node.NodeType.ToSqlOperator();

                _stack.Push(")");
                _stack.Push(rightString);
                _stack.Push(op);
                Visit(left);
                _stack.Push("(");
            }
            else
            {
                _stack.Push(ResolveExpression(node));
            }
        }

        #endregion

        #region 解析表達式

        private string ResolveExpression(Expression node, bool isClause = true)
        {
            switch (node.NodeType)
            {
                case ExpressionType.AndAlso:
                case ExpressionType.OrElse:
                    {
                        var expression = (BinaryExpression)node;
                        var right = ResolveExpression(expression.Right, false);
                        var op = node.NodeType.ToSqlOperator();
                        var left = ResolveExpression(expression.Left, false);

                        return $"({left} {op} {right})";
                    }
                case ExpressionType.MemberAccess:
                    {
                        var expression = (MemberExpression)node;
                        var rootType = expression.GetRootType(out var stack);

                        // 如果是參數表達式
                        if (rootType == ExpressionType.Parameter)
                        {
                            // 如果獨立的語句,如 s.IsActive ,則返回 [列名]=1;
                            return isClause ? $"{ResolveStackToField(stack)}=1" : $"{ResolveStackToField(stack)}";
                        }

                        // 如果不是參數表達式,則計算表達式的值(可能是本地變量、常數等)
                        var val = node.GetValue();
                        if (isClause)   // var isActive=true; s => isActive
                        {
                            if (val is bool b)
                            {
                                return b ? "1=1" : "1=0";
                            }
                        }
                        var parameterName = GetParameterName();
                        Result.Parameters.Add(parameterName, val);
                        return parameterName;
                    }
                case ExpressionType.Call:
                    {
                        // 方法調用
                        var expression = (MethodCallExpression)node;
                        var method = expression.Method.Name;

                        if (expression.Object != null &&
                            expression.Object.NodeType == ExpressionType.MemberAccess)
                        {
                            var rootType = ((MemberExpression)expression.Object).GetRootType(out var stack);
                            if (rootType == ExpressionType.Parameter)
                            {
                                var value = expression.Arguments[0].GetValue();
                                switch (method)
                                {
                                    case "Contains":
                                        value = $"%{value}%";
                                        break;
                                    case "StartsWith":
                                        value = $"{value}%";
                                        break;
                                    case "EndsWith":
                                        value = $"%{value}";
                                        break;
                                }

                                var parameterName = GetParameterName();
                                Result.Parameters.Add(parameterName, value);
                                return $"{ResolveStackToField(stack)} LIKE {parameterName}";
                            }
                            else
                            {
                                var value = node.GetValue();
                                if (isClause)
                                {
                                    if (value is bool b)
                                    {
                                        return b ? "1=1" : "1=0";
                                    }
                                }
                                var parameterName = GetParameterName();
                                Result.Parameters.Add(parameterName, value);
                                return $"{parameterName}";
                            }
                        }
                        else
                        {
                            var value = node.GetValue();
                            if (isClause)
                            {
                                if (value is bool b)
                                {
                                    return b ? "1=1" : "1=0";
                                }
                            }
                            var parameterName = GetParameterName();
                            Result.Parameters.Add(parameterName, value);
                            return $"{parameterName}";
                        }
                    }
                case ExpressionType.Not:
                    {
                        var expression = ((UnaryExpression)node).Operand;
                        if (expression.NodeType == ExpressionType.MemberAccess)
                        {
                            var rootType = ((MemberExpression)expression).GetRootType(out var stack);
                            if (rootType == ExpressionType.Parameter)
                            {
                                return $"{ResolveStackToField(stack)}=0";
                            }
                        }

                        break;
                    }
                // 常量、本地變量
                case ExpressionType.Constant when !isClause:
                    {
                        var val = node.GetValue();
                        var parameterName = GetParameterName();
                        Result.Parameters.Add(parameterName, val);
                        return parameterName;
                    }
                case ExpressionType.Constant:
                    {
                        var expression = (ConstantExpression)node;
                        var value = expression.Value;
                        return value is bool b ? b ? "1=1" : "1=0" : string.Empty;
                    }
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                    {
                        // 二元操作符,等於、不等於、大於、小於等
                        var expression = (BinaryExpression)node;
                        var right = expression.Right;
                        var left = expression.Left;
                        var op = expression.NodeType.ToSqlOperator();

                        if (op == "=" || op == "<>")
                        {
                            if (right.NodeType == ExpressionType.Constant && right.GetValue() == null)
                            {
                                return op == "="
                                    ? $"{ResolveExpression(left, false)} IS NULL"
                                    : $"{ResolveExpression(left, false)} IS NOT NULL";
                            }
                        }

                        return $"{ResolveExpression(left, false)} {op} {ResolveExpression(right, false)}";
                    }
                default:
                    {
                        var value = node.GetValue();
                        if (isClause)
                        {
                            return value is bool b ? b ? "1=1" : "1=0" : string.Empty;
                        }

                        var parameterName = GetParameterName();
                        Result.Parameters.Add(parameterName, value);
                        return parameterName;
                    }
            }

            return string.Empty;
        }

        #endregion

        #region 輔助方法

        protected abstract string ResolveStackToField(Stack<string> parameterStack);

        private string GetParameterName()
        {
            return $"{_prefix}__p_{_parameterIndex++}";
        }

        #endregion
    }
}

查詢條件表達式解釋器 [QueryConditionResolver.cs]

using MyOrm.Reflections;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MyOrm.Expressions
{
    public class QueryConditionResolver<T> : BaseConditionResolver<T>
    {
        public QueryConditionResolver(string prefix = "@") : base(prefix)
        { }

        public QueryConditionResolver(MyEntity entity, string prefix = "@") : base(entity, prefix)
        { }

        protected override string ResolveStackToField(Stack<string> parameterStack)
        {
            switch (parameterStack.Count)
            {
                case 2:
                {
                    // 調用了導航屬性
                    var propertyName = parameterStack.Pop();
                    var propertyFieldName = parameterStack.Pop();

                    MyEntity propertyEntity = Result.NavPropertyList.SingleOrDefault(p => p.Name == propertyName);
                    if(propertyEntity == null)
                    {
                        var prop = Entity.Properties.Single(p => p.Name == propertyName);
                        propertyEntity = MyEntityContainer.Get(prop.PropertyInfo.PropertyType);
                        Result.NavPropertyList.Add(propertyEntity);
                    }
                    
                    var propertyProperty = propertyEntity.Properties.Single(p => p.Name == propertyFieldName);
                    return $"[{propertyName}].[{propertyProperty.FieldName}]";
                }
                case 1:
                {
                    var propertyName = parameterStack.Pop();
                    var propInfo = Entity.Properties.Single(p => p.Name == propertyName);
                    return $"[{Entity.TableName}].[{propInfo.FieldName}]";
                }
                default:
                    throw new ArgumentException("尚未支持大於2層屬性調用。如 student.Clazz.School.Id>10,請使用類似 student.Clazz.SchoolId > 0 替代");
            }
        }
    }
}

更新條件表達式解釋器 [EditConditionResolver.cs]

using MyOrm.Reflections;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MyOrm.Expressions
{
    public class EditConditionResolver<T> : BaseConditionResolver<T>
    {
        public EditConditionResolver(string prefix = "@") : base(prefix)
        { }

        public EditConditionResolver(MyEntity entity, string prefix = "@") : base(entity, prefix)
        { }

        protected override string ResolveStackToField(Stack<string> parameterStack)
        {
            if (parameterStack.Count != 1)
                throw new ArgumentException(
                    "不支持大於1層屬性調用");

            var propertyName = parameterStack.Pop();
            var propInfo = Entity.Properties.Single(p => p.Name == propertyName);
            return $"[{Entity.TableName}].[{propInfo.FieldName}]";
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM