MyBatis动态SQL if条件表达式 大坑

一. 问题背景

  • 近期在项目测试过程中,在使用MyBatis进行列表条件查询时,查询条件是状态为1和状态为2的都可以查到正确的结果,但查询条件是状态为0(无效)时,发现查询出来的结果包含了所有状态(0,1, 2),然后在本地跑测试看了一下sql的执行语句,发现sql根本没有拼接and status=#{status}语句。具体如下:
  • DTO
/**
 * 状态 0:无效 1:有效 2:全部
 */
private Integer status;
  • Mapper.xml
<select id="listQueryResult">
	select * from aTest
	where ent_info_id=#{entInfoId} and type=#{type}
	<if test="status!=null and status!='' and !status.equals(2)">
		and status=#{status}
	</if>
</select>	
  • 执行sql

select * from aTest where ent_info_id=‘123’ and type=1 and status=1 # 当状态为1时
select * from aTest where ent_info_id=‘123’ and type=1 # 当状态为0时

二. 寻找原因

1. MyBatis源码

  1. 通过查看MyBatis源码找到了IfSqlNode类,这个类用于处理动态SQL的<if>节点,方法public boolean apply(DynamicContext context)用来构造节点内的SQL语句。if (evaluator.evaluateBoolean(test, context.getBindings())代码便是解析<if test="status!=null and status!='' and !status.equals(2)">表达式的关键,如果表达式为true则拼接SQL,否则忽略。
// 用于处理动态SQL的<if>节点的类
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();
  }

  @Override
  public boolean apply(DynamicContext context) { // 构造节点内的SQL语句
    if (evaluator.evaluateBoolean(test, context.getBindings())) { // 解析If表达式的关键
      contents.apply(context);
      return true;
    }
    return false;
  }

}
  1. ExpressionEvaluator类:解析表达式使用的是OGNL
public class ExpressionEvaluator {

  public boolean evaluateBoolean(String expression, Object parameterObject) {
    Object value = OgnlCache.getValue(expression, parameterObject); // 表达式的值是从缓存中获取的,由此可知MyBatis竟然对表达式也做了缓存,以提高性能。
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    if (value instanceof Number) {
        return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
    }
    return value != null;
  }
}
  1. OgnlCache类
public final class OgnlCache {
  private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();

  private OgnlCache() {}

  public static Object getValue(String expression, Object root) {
    try {
      Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
      return Ognl.getValue(parseExpression(expression), context, root);
    } catch (OgnlException e) {
      throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
    }
  }

  // 解析表达式的方法:该方法会先从缓存取值,如果没有便进行解析并放入缓存中
  private static Object parseExpression(String expression) throws OgnlException {
    Object node = expressionCache.get(expression);
    if (node == null) {
      node = Ognl.parseExpression(expression);
      expressionCache.put(expression, node);
    }
    return node;
  }
}
  1. 具体的逻辑跟这次问题没有太大关系了,也没有具体看下去,只要知道MyBatis的表达式是用OGNL处理的就可以了。

2. 将对象解释为布尔值结果

如果该对象是一个布尔值,则会提取并返回其值;
如果该对象是一个Number,则将其的双精度浮点值与0进行比较; 非零被视为真,零为假;
否则,当该对象非空时,其结果为真。

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;
  }

3. 结论

  • 因此,如果该对象是一个数值类型(整型、浮点型),当值为0时将被解析为false,否则为true。
  • 对于数值类型,OGNL是这样理解的:’’ == 0 == false。
  • 综上,当出现下述sql时,如果状态status=0,显然0!=’‘是不成立的(因为在这里’'会被当作0,即0!=0),因此导致表达式的值为假,因此不会被拼接。
<if test="status!=null and status!='' and !status.equals(2)">
	and status=#{status}
</if>

三. 问题解决

  • 将sql表达式修改为<if test="status!=null and !status.equals(2)">,该问题就解决啦。该问题的根源还是来自编码的不规范,其实只有字符串类型才需要判断是否!=’’,其他类型完全没有这个必要。

四. Tips:另一个坑

  • 这里有必要再提一个“坑”,如果有类似于下述写法String str ="A"; <if test="str!= null and str == 'A'">时,就要小心了。因为单引号内如果为单个字符时,OGNL将会识别为的Java中的字符类型,这样会导致表达式不成立。解决方法很简单,修改<if test='str!= null and str == "A"'>即可。

参考文献

使用MyBatis的动态SQL表达式时遇到的“坑”(integer)
Mybatis if判断Integer类型的值不等于’‘引发的问题(!=’'等价于!=0)
使用MyBatis动态SQL表达式时遇到的“坑”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值