使用MyBatis的动态SQL表达式时遇到的“坑”(integer)

现有一项目,ORM框架使用的MyBatis,在进行列表查询时,选择一状态(值为0)通过动态SQL拼接其中条件但无法返回正常的查询结果,随后进行排查。

POJO

private Integer status;//状态,可能为0、1、2、3。
//...省略其他

Mapper XML

<sql>
  <trim prefix="where" prefixOverrides="and | or ">
      //...省略其他
      <if test="status != null and status !=''">and status = #{status}</if> 
  <trim prefix="where" prefixOverrides="and | or ">
</sql>

status值为0时该SQL在and status = 0并行正常拼接,也就是说测试的表达式为false,从而导致查询结果错误。但是,显然该值(Integer:0)!= null也!='',应该为true才对。

通过调试MyBatis源码顺藤摸瓜找到了IfSqlNode类,该类用来处理动态SQL的<if>节点,方法public boolean apply(DynamicContext context)用来构造节点内的SQL语句。if (evaluator.evaluateBoolean(test, context.getBindings())该代码便是解析<if test="status !=null and status !=''">测试内表达式的关键,如果表达式为true则拼接SQL,否则忽略。

public class IfSqlNode implements SqlNode {
  private ExpressionEvaluator evaluator;
  private String test;
  private SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
}

打开ExpressionEvaluator类,发现解析表达式使用的是OGNL,如果你使用过古老的Struts框架你应该对它不陌生。通过OgnlCache.getValue(expression, parameterObject);可以看到表达式的值是从缓存中获取的,由此可知MyBatis竟然对表达式也做了缓存,以提高性能。

public class ExpressionEvaluator {  
  public boolean evaluateBoolean(String expression, Object parameterObject) {  
    Object value = OgnlCache.getValue(expression, parameterObject);  
    if (value instanceof Boolean) return (Boolean) value;  
    if (value instanceof Number) return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);  
    return value != null;  
  }

跟进去看看,终于找到了解析表达式的方法private static Object parseExpression(String expression),该方法会先从缓存取值,如果没有便进行解析并放入缓存中,调用然后Ognl.getValue(parseExpression(expression), root)获得表达式的值。

public class OgnlCache {

  private static final Map<String, ognl.Node> expressionCache = new ConcurrentHashMap<String, ognl.Node>();

  public static Object getValue(String expression, Object root) throws OgnlException {
    return Ognl.getValue(parseExpression(expression), root);
  }

  private static Object parseExpression(String expression) throws OgnlException {
    try {
      Node node = expressionCache.get(expression);
      if (node == null) {
        node = new OgnlParser(new StringReader(expression)).topLevelExpression();
        expressionCache.put(expression, node);
      }
      return node;
    } catch (ParseException e) {
      throw new ExpressionSyntaxException(expression, e);
    } catch (TokenMgrError e) {
      throw new ExpressionSyntaxException(expression, e);
    }
  }

至于Ognl.getValue(parseExpression(expression), root)的英文如何运作的,如果你有兴趣可以自行跟下去一探究竟,本文就不赘述了。到此为止,我们已经知道的MyBatis的表达式是用OGNL处理的了,这一点已经够了。下面我们去OGNL网官看看的英文不是我们的表达式语法有问题从而导致该问题的发生。

将对象解释为布尔值

任何对象都可以在需要布尔值的地方使用。OGNL将对象解释为这样的布尔值:

  1. 如果该对象是一个布尔值,则会提取并返回其值;
  2. 如果该对象是一个Number,则将其双精度浮点值与零进行比较; 非零被视为真,零为假;
  3. 如果对象是一个字符,那么它的布尔值是true,当且仅当它的char值非零时;
  4. 否则,当且仅当它非空时,其布尔值才为真。

果然,如果对象是一个数类型,值为0时将被解析为false,否则为true,浮点型0.00也是如此.OGNL对于布尔的定义和JavaScript的有点像,即'' == 0 == false。也就这不难理解<if test="status != null and status !=''">and status = #{status}</if>当状态= 0时出现的问题了,显然0!=''的英文不成立的,导致表达式的值为假。

表达式将修改为<if test="status != null">and status = #{status}</if>该问题便迎刃而解。该问题的根源还是来自编码的不规范,只有字符串类型才需要判断是否!='',其他类型完全没有这个必要,可能是开发人员为了省事直接复制上一行拿过来改一改或是所使用的MyBatis的生成工具不严谨导致该问题的发生

这里有必要再提一个“坑”,如果有你类似于String str ="A";<if test="str!= null and str == 'A'">这样的写法时,你要小心了。单因为引号内如果为单个字符时,OGNL将会识别为的Java中的字符类型,显然字符串类型与焦炭做类型==运算会报道查看false,从而导致表达式不成立。解决方法很简单,为修改<if test='str!= null and str == "A"'>即可。


作者:简单的土豆
链接:HTTP://www.jianshu.com/p/91ed365c0fdd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原文地址:https://www.cnblogs.com/pzx-java/p/9020267.html