一. 问题背景
- 近期在项目测试过程中,在使用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源码
- 通过查看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;
}
}
- 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;
}
}
- 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;
}
}
- 具体的逻辑跟这次问题没有太大关系了,也没有具体看下去,只要知道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表达式时遇到的“坑”