最近遇到一个问题,应用层提供的SQL语句中有
CAST(local_time AS DATE)
这样的语句,在MySQL中执行肯定是没问题的,但是后台数据库切换到了HBase,使用apache phoenix 提供的JDBC驱动访问时却报错了,按照phoenix官方的文档,CAST函数是支持,但现实就是报错过不了,应该是我使用的phoenix版本问题,应该是个BUG,暂时无法通过升级版本解决。
解决方案也不复杂就是用phoenix的Native函数
TO_DATE,TO_CHAR
函数来代替,将
CAST(local_time AS DATE)
替换为
TO_DATE(TO_CHAR("local_time"), 'yyyy-MM-dd')
。
那么问题来了,如果让应用层来替换这事很方便,但是我们希望数据存储对应用层是透明的,应用层不需要知道存储是MySQL还是HBase,如果让应用层修改,那应用层就需要知道数据库的类型是MySQL还是HBase,根据不同的数据库使用不同的SQL语句,这太麻烦了----这是下下策。
有没有可能在服务端自动替换呢?有了
jsqlparser
这个神器,这个想法就可以实现。
jsqlparser是一个java的SQL语句解析器,在我之前的博客:
《jsqlparser:基于抽象语法树(AST)遍历SQL语句的语法元素》
以及
《jsqlparser:实现基于SQL语法分析的SQL注入攻击检查》
介绍了如何通过jsqlparser来遍历SQL语句中所有的语法单元实现自己的需求。
那么基于jsqlparser解析出的对象,修改部分语法也是可以实现的。所以一个基本的解决思路就有了:
遍历jsqlparser解析的语法树对象,找到所有CAST函数(Function对象),将其替换为需要的函数。
以下是实现代码:
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
import net.sf.jsqlparser.util.TablesNamesFinder;
import java.util.function.Consumer;
* 基于SQL语法对象实现对SQL的修改<br>
* 对PHOENIX支持有问题的CAST日期函数转换为使用PHOENIX的Native函数TO_DATE,TO_TIME,TO_TIMESTAMP
* @author guyadong
public class PhoenixNormalizer extends TablesNamesFinder{
public PhoenixNormalizer() {
* 根据输入参数创建TO_DATE,TO_TIME,TO_TIMESTAMP函数
* @param castLeftExpression CAST的值参数
* @param format 时间格式参数
* @param targetFunctionName 要创建的Function对象的函数名
private Expression castToFunction(Expression castLeftExpression,String format,String targetFunctionName){
Function toChar = new Function()
.withName("TO_CHAR")
.withParameters(new ExpressionList().addExpressions(castLeftExpression));
Function targetFunction = new Function()
.withName(targetFunctionName)
.withParameters(new ExpressionList().addExpressions(toChar,new StringValue(format)));
return targetFunction;
* 从 Cast 函数中获取对应的参数,将其转为对应的TO_DATE,TO_TIME,TO_TIMESTAMP函数
private void onCastExpression(CastExpression castExpression,Consumer<Expression>consumer){
Expression newExp = null;
switch (castExpression.getType().toString().toLowerCase()) {
case "date":{
newExp = castToFunction(castExpression.getLeftExpression(),"yyyy-MM-dd","TO_DATE");
break;
case "time":{
newExp = castToFunction(castExpression.getLeftExpression(),"yyyy-MM-dd HH:mm:ss","TO_TIME");
break;
case "timestamp":{
newExp = castToFunction(castExpression.getLeftExpression(),"yyyy-MM-dd'T'HH:mm:ss.SSSZ","TO_TIMESTAMP");
break;
default:
break;
if(null != newExp){
consumer.accept(newExp);
@Override
public void visit(SelectExpressionItem item) {
* 不同于其他函数,jsqlparser对于CAST函数是单独处理的,定义了一个单独的类 CastExpression
if(item.getExpression() instanceof CastExpression){
onCastExpression((CastExpression)item.getExpression(),item::setExpression);
super.visit(item);
Statement stmt;
String sql = "SELECT count(1) AS count, CAST(\"local_time\" AS date) AS datastr, \"device_id\", \"media_id\" FROM \"dc_play_log_hbase\" WHERE \"local_time\" < TO_TIMESTAMP(\'2022-11-30 23:59:59\') AND \"local_time\" >= TO_TIMESTAMP(\'2022-11-22 00:00:00\') GROUP BY datastr,\"device_id\", \"media_id\""
stmt = CCJSqlParserUtil.parse(sql);
stmt.accept(new PhoenixNormalizer());
System.printf("sql = %s\n",stmt);
sql = SELECT count(1) AS “count”, TO_DATE(TO_CHAR(“local_time”), ‘yyyy-MM-dd’) AS “datastr”, “device_id”, “media_id” FROM “dc_play_log_hbase” WHERE “local_time” < TO_TIMESTAMP(‘2022-11-30 23:59:59’) AND “local_time” >= TO_TIMESTAMP(‘2022-11-22 00:00:00’) GROUP BY “datastr”, “device_id”, “media_id”
完整的代码参见码云仓库:
https://gitee.com/l0km/sql2java/blob/master/sql2java-manager/src/main/java/gu/sql2java/phoenix/PhoenixNormalizer.java
单元测试:
https://gitee.com/l0km/sql2java/blob/master/sql2java-manager/src/test/java/gu/sql2java/pagehelper/parser/JsqlParserTest.java
参考资料:
《apache functions》
https://gitee.com/l0km/JSqlParser.git 【gyd分支,gyd分支,gyd分支,重要的事情说三遍】为了这个我翻了jsqlparser的源码,找到了jsqlparser的语法定义文件(JSqlParserCC.jjt),是基于。为了看懂语法定义文件(JSqlParserCC.jjt),我又恶补了一下javacc,但是jsqlparser目前的最新版本支持UPSET语法,也支持。打开它在1635行就能找到UPSERT语句分析。语句的位置,如下修改增加对。
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.1</version>
intercept:在拦截时,需要执行的业务逻辑
plugin:是否代理Executor、ParameterHandler、ResultSetHandler、StatementHandler对象中的某个或某些,
如果代理,则返回相应对象的代理对象,否则返回原对象。根据类上Intercepts注解决定是否返回代理对象。
setProp
sqlparser是一个java的SQL语句解析器,在上一篇博客:《jsqlparser:基于抽象语法树(AST)遍历SQL语句的语法元素》介绍了如何通过jsqlparser来遍历SQL语句中所有的字段和表名引用。
其实它可以用来进行更复杂的工作,jsqlparser会将一条SQL语句的各种语法元素以抽象语法树(AST,abstract syntax tree)形式解析为很多不同类型对象,通过对AST的遍历就可以对SQL语句进行分析。采用这种方式做SQL注入攻击检查不会有误判,漏判的问题。
Servlet.service() for servlet [dispatcherServlet] in context with path [/resource] threw exception [Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: net/sf/jsqlparser/expression/Function] with root cause
java.lang.ClassNotFound
1、 jsqlparse介绍
JSqlParse是一款很精简的sql解析工具,它可以将常用的sql文本解析成具有层级结构的“语法树”,我们可以针对解析后的“树节点(也即官网里说的有层次结构的java类)”进行处理进而生成符合我们要求的sql形式。
官网给的介绍很简洁:JSqlParser 解析 SQL 语句并将其转换为 Java 类的层次结构。生成的层次结构可以使用访问者模式进行访问(官网地址:JSqlParser - Home)。
官网的介绍即是该中间件的全部,虽然介绍很短,但是其功能着实强悍。
String xxx = " AUTO_INCREMENT ";
CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(" CREATE TABLE `t_student` (\n" +
" `student_id` int(10) unsigned NO
graph包下的类,解决DAG矢量图问题(算子之间的顺序关系),不是本文重点,主要讲jsqlparser
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
我们可以借助JSqlparser来解析SQL并且动态拼接生成SQL,在Mybatis-plus中的租户其实也是类似这样实现的。甚至有兴趣的同学可以自己做一个SQL拼装器,将前台筛选的条件转换为SQL进行查询。所有的查询字段、条件、联表等等都做成动态拼装。............