mybatis 中 if-test 判断大坑

【<if test="takeWay =='0'">】mybatis 的 if 判断

单个的字符要写到双引号里面才行,改为 <if test='takeWay =="1"'> 或者改为 <if test="takeWay == '1'.toString() ">

.xml 文件的部分代码

<insert id="insertDelivery" parameterType="com.zuci.request.DeliveryPreferenceReq">     
    insert cx_customer_deliverypreference     
    <trim prefix="(" suffix=")" suffixOverrides=",">           
        .... 此处省略       
        <if test="takeWay =='1'and workday != null">         
            WORKDAY,       
        </if>       
        ....     
    </trim>
         
    <trim prefix="values (" suffix=")" suffixOverrides=",">          
        .... 此处省略          
        <if test="takeWay =='1'and workday != null">            
            #{workday, jdbcType=VARCHAR},     
        </if>      
        ....   
    </trim>
</insert>

takeWay == “1”处出错,导致不执行 if 判断中的 sql,运行程序不报错,没有任何提示。去掉 takeWay == “1” and 则可执行。对此我百思不得其解,

因为自己有写过如下代码,是没错的。

<if test="messageType =='senderReceiveSuccess' ">
      ......
</if>

把 <if test="takeWay =='1'and workday != null">

改为 <if test='takeWay =="1"and workday != null'>

或改为 <if test="takeWay =='1'.toString() and workday != null"> 即可。

原因是:mybatis 是用 OGNL 表达式来解析的,在 OGNL 的表达式中,’1’会被解析成字符,java 是强类型的,char 和 一个 string 会导致不等,所以 if 标签中的 sql 不会被解析。

总结下使用方法:单个的字符要写到双引号里面或者使用.toString() 才行!

 

使用 Mybatis 时,常常会判断属性是否为空

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 时该 where SQL and status = 0并未正常拼接,也就是说 test 内的表达式为false,从而导致查询结果错误。但是,显然该值(Integer :0)!= null 也!= ' ',应该为true才对。

当 status 为 Integer 类型,并且 status 值为 0 时,该 if 判断却为 false。

当 status 为 0 时,Mybatis 会解析成 '' 空字符串。

为了避免这个问题,改成下面这样写,去掉对空字符的判断,就解决了该问题

<if test="status != null">and status = #{status}</if> 

 

原因分析

通过 Debug MyBatis 源码顺藤摸瓜找到了IfSqlNode类,该类用来处理动态 SQL 的 <if> 节点,方法public boolean apply(DynamicContext context)用来构造节点内的 SQL 语句。if (evaluator.evaluateBoolean(test, context.getBindings())该代码便是解析<if test="status !=null and status !=''">test 内表达式的关键,如果表达式为 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 官网看看是不是我们的表达式语法有问题从而导致该问题的发生。

Interpreting Objects as Booleans

Any object can be used where a boolean is required. OGNL interprets objects as booleans like this:

  • If the object is a Boolean, its value is extracted and returned;
  • If the object is a Number, its double-precision floating-point value is compared with zero; non-zero is treated as true, zero as false;
  • If the object is a Character, its boolean value is true if and only if its char value is non-zero;
  • Otherwise, its boolean value is true if and only if it is non-null.

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

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

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