【手撸一个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[].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 :
{
// 调用了导航属性
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 :
{
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 != )
throw new ArgumentException(
"不支持大于1层属性调用"); var propertyName = parameterStack.Pop();
var propInfo = Entity.Properties.Single(p => p.Name == propertyName);
return $"[{Entity.TableName}].[{propInfo.FieldName}]";
}
}
}
【手撸一个ORM】第五步、Expression(表达式目录树)转换为Where子句的更多相关文章
- 【手撸一个ORM】第一步、实体约定和描述
		
一.约定 数据实体必须实现 IEntity 接口,该接口定义了一个int类型的Id属性,既每个实体必须有一个名称为Id的自增主键. 若数据表的主键列名称不是Id,可以通过 [MyKey("主 ...
 - 【手撸一个ORM】第九步、orm默认配置类 MyDbConfiguration,一次配置,简化实例化流程
		
这个实现比较简单,事实上可配置的项目很多,如有需要,请读者自行扩展 using System; namespace MyOrm { public class MyDbConfiguration { p ...
 - 【手撸一个ORM】MyOrm的使用说明
		
[手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...
 - 第十五节:Expression表达式目录树(与委托的区别、自行拼接、总结几类实例间的拷贝)
		
一. 基本介绍 回忆: 最早接触到表达式目录树(Expression)可能要追溯到几年前使用EF早期的时候,发现where方法里的参数是Expression<Func<T,bool> ...
 - 【学习笔记】Expression表达式目录树
		
Expression表达式目录树:一个能拼装能解析的数据结构,语法树. 一.手动拼装表达式目录树 示例1: /// <summary> /// 展示表达式树,协助用的 /// 编译lamb ...
 - 【手撸一个ORM】第四步、Expression(表达式目录树)扩展
		
到这里,Orm的基架已经搭起来了,接下来就是激动人心的部分,表达式目录树转Sql语句,SqlDataReader转数据实体等等,但是在这之前,我们需要扩展下表达式目录树的方法,以方便后面的相关操作. ...
 - 【手撸一个ORM】第六步、对象表达式解析和Select表达式解析
		
说明 一个Orm自然不仅仅包含条件表达式,还会有如下的场景: OrderBy(s => s.StudentName) Select<StudentDto>(s => new S ...
 - 【手撸一个ORM】第七步、SqlDataReader转实体
		
说明 使用Expression(表达式目录树)转Entity的文章在园子里有很多,思路也大致也一样,我在前面有篇文章对解决思路有些说明,有兴趣的小伙伴可以看下 (传送门),刚接触表达式目录树时写的,不 ...
 - Expression表达式目录树
		
一.初识Expression 1.在上一篇我们讲到了委托(忘记了可以在看看,点赞在看养成习惯),今天要讲的Expression也和委托有一点点关系吧(没有直接关系,只是想要大家看看我其他的文章),Ex ...
 
随机推荐
- laravel基础课程---16、数据迁移(数据库迁移是什么)
			
laravel基础课程---16.数据迁移(数据库迁移是什么) 一.总结 一句话总结: 是什么:数据库迁移就像是[数据库的版本控制],可以让你的团队轻松修改并共享应用程序的数据库结构. 使用场景:解决 ...
 - java中相对路径加载xml
			
一.xml文件一般的存放位置有三个: 1.放在WEB-INF下: 2.xml文件放在/WEB-INF/classes目录下或classpath的jar包中: 3.放在与解析它的java类同一个包中,不 ...
 - leetcode 104 Maximum Depth of Binary Tree(DFS)
			
Given a binary tree, find its maximum depth. The maximum depth is the number of nodes along the long ...
 - star score
			
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...
 - poj 2689Prime Distance(区间素数)埃氏筛法
			
这道题的L和R都很大,所以如果直接开一个1~R的数组明显会超时.但是R-L并不大,所以我们考虑把这个区间(L--R)移动到(1--(R-L+1))这个区间再开数组(就是把每个数减L再加1).接下来先用 ...
 - ceph 删除了osd但是osd目录保存完整如何恢复
			
1. 这里假设有一个osd被删除了 执行下列步骤删除: ceph osd out osd.0 service ceph stop osd.0 ceph osd crush remove osd.0 c ...
 - JavaScript高级程序设计学习笔记第十三章--事件
			
事件冒泡: IE 的事件流,事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档).例如: <!DOCTYPE html> <htm ...
 - iView之select获取value和label
			
使用:label-in-value="true" @on-change="obtainValue" 详见官方文档:https://www.iviewui.com ...
 - CodeForces 484A Bits(水题)
			
A. Bits time limit per test 1 second memory limit per test 256 megabytes input standard input output ...
 - Linux 系统初始化和服务
			
系统的初始化和服务 1. Linux 系统启动流程 打开计算机,从主板 BIOS(Basic Input/Out System)读取其中所存储的程序,引导你找到存储系统的硬件(如光盘.硬盘等) 接下来 ...