mybatis IF判断的坑

探讨MyBatis中IF标签的SQL生成机制,解析其内部实现原理,解决因类型不匹配导致的条件判断失效问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在项目使用mybatis中碰到个问题 

<if test="type=='y'">  
    and status = 0   
</if>  


当传入的type的值为y的时候,if判断内的sql也不会执行,抱着这个疑问就去看了mybatis是怎么解析sql的。下面我们一起来看一下mybatis 的执行过程。 

DefaultSqlSession.class  121行 

public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {  
    try {  
      MappedStatement ms = configuration.getMappedStatement(statement);  
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);  
    } catch (Exception e) {  
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
    } finally {  
      ErrorContext.instance().reset();  
    }  
  }  


在 executor.query(ms, wrapCollection(parameter), rowBounds, handler); 
执行到BaseExecutor.class执行器中的query方法 

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {  
    BoundSql boundSql = ms.getBoundSql(parameter);  
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);  
 }  

在query的方法中看到boundSql,是通过 ms.getBoundSql(parameter);获取的。 

再点进去可以看到MappedStatement.class类中的getBoundSql方法 

public BoundSql getBoundSql(Object parameterObject) {  
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);  
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
    if (parameterMappings == null || parameterMappings.size() <= 0) {  
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);  
    }  
  
    // check for nested result maps in parameter mappings (issue #30)  
    for (ParameterMapping pm : boundSql.getParameterMappings()) {  
      String rmId = pm.getResultMapId();  
      if (rmId != null) {  
        ResultMap rm = configuration.getResultMap(rmId);  
        if (rm != null) {  
          hasNestedResultMaps |= rm.hasNestedResultMaps();  
        }  
      }  
    }  
  
    return boundSql;  
  }  


看到其中有sqlSource.getBoundSql(parameterObject); sqlsource是一个接口。 

/** 
 *  
 * This bean represets the content of a mapped statement read from an XML file 
 * or an annotation. It creates the SQL that will be passed to the database out 
 * of the input parameter received from the user. 
 *  
 */  
public interface SqlSource {  
  
  BoundSql getBoundSql(Object parameterObject);  
  
}  

类中getBoundSql是一个核心方法,mybatis 也是通过这个方法来为我们构建sql。BoundSql 对象其中保存了经过参数解析,以及判断解析完成sql语句。比如<if> <choose> <when> 都回在这一层完成,具体的完成方法往下看,那最常用sqlSource的实现类是DynamicSqlSource.class 

public class DynamicSqlSource implements SqlSource {  
  
  private Configuration configuration;  
  private SqlNode rootSqlNode;  
  
  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {  
    this.configuration = configuration;  
    this.rootSqlNode = rootSqlNode;  
  }  
  
  public BoundSql getBoundSql(Object parameterObject) {  
    DynamicContext context = new DynamicContext(configuration, parameterObject);  
    rootSqlNode.apply(context);  
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);  
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();  
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());  
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);  
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {  
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());  
    }  
    return boundSql;  
  }  
  
}  

核心方法是调用了rootSqlNode.apply(context); rootSqlNode是一个接口 

public interface SqlNode {  
  boolean apply(DynamicContext context);  
}  

可以看到类中 rootSqlNode.apply(context); 的方法执行就是一个递归的调用,通过不同的 
实现类执行不同的标签,每一次appll是完成了我们<></>一次标签中的sql创建,计算出标签中的那一段sql,mybatis通过不停的递归调用,来为我们完成了整个sql的拼接。那我们主要来看IF的实现类IfSqlNode.class 

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


可以看到IF的实现中,执行了 if (evaluator.evaluateBoolean(test, context.getBindings()))  如果返回是false的话直接返回,否则继续递归解析IF标签以下的标签,并且返回true。那继续来看 evaluator.evaluateBoolean 的方法 

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


关键点就在于这里,在OgnlCache.getValue中调用了Ognl.getValue,看到这里恍然大悟,mybatis是使用的OGNL表达式来进行解析的,在OGNL的表达式中,'y'会被解析成字符,因为java是强类型的,char 和 一个string 会导致不等。所以if标签中的sql不会被解析。具体的请参照 OGNL 表达式的语法。到这里,上面的问题终于解决了,只需要把代码修改成: 

<if test='type=="y"'>  
    and status = 0   
</if>  

就可以执行了,这样"y"解析出来是一个字符串,两者相等!

 

原文:https://cheng-xinwei.iteye.com/blog/2008200

<think>我们正在处理一个关于MyBatis中`<if test>`标签进行数字大于等于判断的问题。 根据引用[2]和引用[3]的内容,我们知道在MyBatis中做条件判断时,对于数字类型需要特别注意。 引用[2]指出:当id类型为int,且id=0时,判断`<if test="id != null and id != ''">`不会进入,因为0被当作空字符串处理了?实际上,这里的问题在于,当数字为0时,在MyBatis的OGNL表达式中,0会被当作false来处理,所以我们需要特别小心。 对于数字的大于等于判断,我们需要注意以下几点: 1. 在MyBatis的`<if test>`中,我们可以直接使用比较运算符(如>=, >, <, <=, ==, !=)来比较数字。 2. 但是,当比较的数字是传入的参数时,我们需要确保参数是数字类型,并且不为null(如果需要避免空指针,可以同时判断非空)。 3. 另外,如果参数可能为0,我们也要注意0在条件中的处理,因为0在数字比较中是有效的。 例如,我们有一个参数`age`,我们想判断`age`是否大于等于18,我们可以这样写: <if test="age != null and age >= 18"> ... </if> 但是,这里有一个潜在的:如果`age`是0,那么0>=18是不成立的,所以条件不会进入,这是符合逻辑的。但是,如果我们同时需要判断0的情况(比如0代表特殊含义),那么我们需要单独处理。 另外,引用[3]提到,当判断条件为等于时,对于字符串常量需要加`.toString()`,但对于数字的等于判断,可以直接使用`==`。然而,对于大于等于,我们直接使用`>=`即可。 因此,针对数字的大于等于判断,我们可以直接使用`>=`运算符,同时注意非空判断(如果参数可能为null的话)。 示例: 假设我们有一个整数类型的参数`score`,我们想判断`score`是否大于等于60,那么我们可以这样写: <if test="score != null and score >= 60"> AND status = 'PASS' </if> 如果参数是基本数据类型(如int),那么它不会为null,所以可以省略null判断,直接写: <if test="score >= 60"> AND status = 'PASS' </if> 但是,如果参数是Integer类型(即可能为null),那么就需要先判断非空。 另外,引用[4]提到使用equals方法可能不稳定,所以对于数字比较,我们直接使用比较运算符,不要尝试用equals。 总结:在MyBatis中,对于数字的大于等于判断,可以直接使用`>=`运算符,同时根据参数是否可为空来决定是否添加非空判断。 注意:在XML中,大于号(>)和小于号(<)需要转义,但是大于等于(>=)中的大于号不需要转义,因为后面紧跟的是等号,不会被解析为标签的开始。但是,如果单独使用大于号,为了避免解析错误,我们通常使用转义字符,例如:`>`。然而,在`>=`中,由于后面不是空格或字母,所以可以直接写。但是,为了安全起见,我们可以将整个表达式用CDATA包裹,或者使用转义。不过,在MyBatis的test属性中,通常直接写>=是没有问题的。 但是,如果我们在XML中写`>=`,可能会被某些解析器认为是不合法的(因为>是标签的开始),但实际上在属性值中,这样的写法是被允许的。如果担心,我们可以将大于等于号用转义写法:`>=`。 因此,我们可以这样写: <if test="score != null and score >= 60"> ... </if> 或者直接写(大多数情况下没有问题): <if test="score != null and score >= 60"> ... </if> 根据MyBatis官方文档,在OGNL表达式中,比较运算符可以直接使用,包括:>, <, >=, <=, ==, !=。所以,我们推荐直接使用这些运算符。 因此,回答用户的问题:在MyBatis中,使用`<if test>`进行数字大于等于判断,可以直接使用`>=`运算符,并注意参数是否为null。 下面是一个完整的例子: <select id="selectStudents" resultType="Student"> SELECT * FROM student WHERE 1=1 <if test="minScore != null and minScore >= 60"> AND score >= #{minScore} </if> </select> 在这个例子中,如果传入的minScore大于等于60,则会添加条件。 注意:在条件中,我们使用了`score >= #{minScore}`,这里的大于等于号在SQL中也是直接写`>=`,因为SQL中就是这样写的。 但是,在`<if test>`中的表达式是OGNL表达式,使用`>=`;在SQL语句中,我们同样使用`>=`,但要注意在XML中,SQL语句中的大于等于号可以写在CDATA块中,或者直接写(因为不在标签属性中,所以不需要转义)。不过,在SQL中直接写`>=`是安全的。 如果SQL语句中需要写大于号或小于号,为了避免XML解析错误,我们通常使用CDATA包裹,例如: AND score <![CDATA[ >= ]]> #{minScore} 但是,在SQL中,`>=`并不是XML的保留字符,所以直接写也是可以的。不过,为了保险起见,很多开发者习惯在SQL的条件判断中使用CDATA来包裹包含比较符号的条件。 因此,我们可以这样写: AND score >= #{minScore} <!-- 使用转义 --> 或者 AND score >= #{minScore} <!-- 直接写,在SQL中一般没问题 --> 或者 AND <![CDATA[ score >= #{minScore} ]]> <!-- 使用CDATA,确保万无一失 --> 但是,在`<if test>`中,我们已经在属性值里面了,所以直接写`>=`或者使用转义`>=`都可以。 综上所述,我们给出如下答案:</think>在 MyBatis 中使用 `<if test>` 标签进行数字的**大于等于(≥)判断**时,需注意 OGNL 表达式对数字类型和运算符的处理规则。以下是具体实现方法和注意事项: --- ### 一、正确语法示例 ```xml <select id="selectUsers" resultType="User"> SELECT * FROM user WHERE 1=1 <!-- 判断整数参数 age 是否大于等于 18 --> <if test="age != null and age >= 18"> <!-- 推荐写法 --> AND is_adult = 1 </if> <!-- 判断浮点数参数 score 是否大于等于 60.0 --> <if test="score != null and score >= 60.0"> AND is_passed = 1 </if> </select> ``` --- ### 二、关键注意事项 1. **数字类型直接比较** 在 OGNL 表达式中,可直接使用 `>=` 进行数值比较(无需 `.toString()` 或特殊转换): ```xml <if test="numParam >= 100"> <!-- 合法 --> ``` 2. **空值安全判断** 若参数可能为 `null`,需优先验证非空,避免 `NullPointerException`: ```xml <if test="numParam != null and numParam >= 100"> <!-- 正确 --> ``` 3. **整数与浮点数的区别** - 整数比较:`age >= 18` - 浮点数比较:`score >= 60.0` (显式声明小数位更严谨) 4. **避免混淆运算符** - 错误:`>=`(XML 转义符不适用于 OGNL) - 正确:直接使用 `>=` --- ### 三、常见错误场景 #### 错误示例 1:未处理 null 值 ```xml <if test="age >= 18"> <!-- 若 age 为 null 会抛异常 --> ``` **修复**: ```xml <if test="age != null and age >= 18"> ``` #### 错误示例 2:误用字符串比较 ```xml <if test="age == '18'"> <!-- 将数字与字符串比较,逻辑错误 --> ``` **修复**: ```xml <if test="age == 18"> <!-- 直接比较数字 --> ``` --- ### 四、不同类型参数的处理对比 | 参数类型 | 正确写法 | 错误写法 | |----------------|-----------------------------------|-----------------------------| | `Integer` 参数 | `age != null and age >= 18` | `age >= '18'` | | `int` 基本类型 | `age >= 18` | `age != null and age >= 18`(多余) | | `Double` 参数 | `score != null and score >= 60.0` | `score >= 60`(丢失精度) | --- ### 五、引用说明 MyBatis 的 OGNL 表达式在处理数字比较时需确保类型匹配: - 数字类型直接使用数学运算符(`>`, `>=`, `<`, `<=`)[^1][^2] - 避免混淆字符串与数字的比较逻辑(如误用 `== '1'` 导致错误)[^3][^4] - 基本类型无需判空,包装类型需优先检查 `null`[^2] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值