基于对象导航图语言(OGNL)的动态 SQL 生成实现原理

  • 目录

    • 前言

    • 查找并替换动态查询模板中的变量

    • 如何计算查询模板中的变量或者表达式

    • 如何设计大众易接受的语法规则和查询模板

    • 如何从查询模板中解析动态标签并处理

本文来源于微信公众号:码上观世界,本文为完整首发版。

前言

在可视化图表应用中,考虑到性能问题,一个查询命令通常对应一个或者多个结果数据源表(结果数据源表可以来源于关系型数据库或者是非关系型的数据仓库),通过配置结果查询表的维度和度量,返回动态查询数据,然而这种基于配置维度和度量的方式不太适合频繁更改查询语句和条件的交互式查询场景,因为基于配置的方式,查询语句是静态的、确定的,如果修改查询语句意味着图表生成逻辑的变更。对于复杂的查询,自定义关联查询不仅对应后端几个数据源表的,而且筛选条件也是根据输入条件动态变化,对于底层数据源表本身数据不规范,导致多个图表使用同一份数据源时候,需要做较多的逻辑加工。比如如下的图表查询:

其中某些条件是可以不选的,如分公司、客户类型、服务站,某些是必选的如年、月。针对这些问题,后端功能如何设计才能一劳永逸地满足不同的需求场景呢?此时非动态模板查询难以胜任,使用过 MyBatis 的同学已经体验过其强大之处,但是在我们的场景,还不能直接使用这么复杂的框架,能否借鉴其思想,将其部分功能迁移过来呢?理论上和实际上其实都是可能的,但是需要解决以下几个问题:

  1. 如何查找并替换动态查询模板中的变量?虽然可以通过正则表达式替换字符串,但是这种方式不仅效率低,也没有做到变量查找和替换动作的分离,而且也不适合复杂表达式运算;

  2. 如何计算查询模板中的变量或者表达式?这里的表达式计算不仅包括算术、逻辑运算,还包括对象属性和方法调用等;

  3. 如何设计大众易接受的语法规则和查询模板,比如 MyBatis 中查询模板支持的常见节点标签,如 If 逻辑节点、动态 Where 节点等?

  4. 如何从查询模板中解析动态标签,并通过优雅的处理逻辑实现?

现在带着上面几个问题,开始破题之旅吧!

查找并替换动态查询模板中的变量

首先以一个常规思路来看看,我们使用 JDK 里面的 java.text.MessageFormat 来测试:

@Test
public void textMessageFormat(){
    String template="select * from people where name=\"{0}\" and sex=\"{1}\"";
    Object[] params = new Object[]{"jack", "female"};
    String msg = MessageFormat.format(template, params);
    System.out.println(msg);
}

从测试效果来看,该用法暂不支持单引号,双引号还要加上转义符,而且通过数字占位符可读性太差,当变量比较多的时候,代码的可维护性也是一个大问题。接下来看看 commons-text 中的 StringSubstitutor:

@Test
public void stringSubstitute(){
    String template="select * from people where name='${name}' and sex=${sex}";
    Map valuesMap = new HashMap();
    valuesMap.put("name", "jack");
    valuesMap.put("sex", "female");
    StringSubstitutor sub = new StringSubstitutor(valuesMap);
    String target = sub.replace(template);
    System.out.println(target);
}

从测试结果来看基本符合预期,但是对于自定义占位符和自定义替换规则无法支持,需要探讨更高级的玩法。

基本设计思路就是通过分离查找和处理逻辑,根据自定义的占位符开始和结束标记查找占位符的开始位置和结束位置,通过截取两个位置中间的字符串取出占位符变量,然后应用自定处理器,代码实现为:

public class GenericTokenParser {
    private final String openToken;
    private final String closeToken;
    private final TokenHandler handler;


    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
    }


    public String parse(String text) {
        if (text == null || text.isEmpty()) {
            return "";
        }
        // search open token
        int start = text.indexOf(openToken);
        if (start == -1) {
            return text;
        }
        char[] src = text.toCharArray();
        int offset = 0;
        final StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;
        do {
            if (start > 0 && src[start - 1] == '\\') {
                // this open token is escaped. remove the backslash and continue.
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                // found open token. let's search close token.
                if (expression == null) {
                    expression = new StringBuilder();
                } else {
                    expression.setLength(0);
                }
                builder.append(src, offset, start - offset);
                offset = start + openToken.length();
                int end = text.indexOf(closeToken, offset);
                while (end > -1) {
                    if (end > offset && src[end - 1] == '\\') {
                        // this close token is escaped. remove the backslash and continue.
                        expression.append(src, offset, end - offset - 1).append(closeToken);
                        offset = end + closeToken.length();
                        end = text.indexOf(closeToken, offset);
                    } else {
                        expression.append(src, offset, end - offset);
                        break;
                    }
                }
                if (end == -1) {
                    // close token was not found.
                    builder.append(src, start, src.length - start);
                    offset = src.length;
                } else {
                    builder.append(handler.handleToken(expression.toString()));
                    offset = end + closeToken.length();
                }
            }
            start = text.indexOf(openToken, offset);
        } while (start > -1);
        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}

其中 handler 是一个接口,可以实现自定义的处理逻辑:

public interface TokenHandler {
  String handleToken(String content);
}

使用方式形如:

@Testpublic void testHandler() {    final HashMap<String, String> parameterObject = new HashMap<String, String>() {{        put("name", "jack");        put("sex", "female");    }};    String template="select * from people where name='${name}' and sex=${sex}";    BindingTokenHandler handler=new BindingTokenHandler(parameterObject);    GenericTokenParser parser=new GenericTokenParser("${", "}", handler);    String target=parser.parse(template);    System.out.println(target);}

如何计算查询模板中的变量或者表达式


模板中的变量通常包含原子变量、算数运算表达式、逻辑运算表达式、对象属性存取、对象方法调用、集合操作、投影操作等,这些表达式其实就是根据对象图来获取的,所谓对象图就是将对象结构解析成树状结构,存取对象值就是根据树状节点路径获得相应的值。下面结合开源的对象图导航语言 OGNL(Object-Graph Navigation Language)来演示这些表达式的使用方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值