目 录CONTENT

文章目录

MyBatis中if判断的坑

筱晶哥哥
2022-03-25 / 0 评论 / 0 点赞 / 68 阅读 / 11312 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-03-23,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

最近在项目使用 MyBatis 中碰到个问题,这个问题可能微不足道,但是还是拎出来讲一讲。

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

当传入的 type 的值为 'y' 的时候,if 判断内的 sql 不会执行。

抱着这个疑问就去看了 MyBatis 是怎么解析 sql 的。

下面我们一起来看一下MyBatis 的执行过程。

DefaultSqlSession.java 120行

@Override
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
        registerCursor(cursor);
        return cursor;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

executor.queryCursor(ms, wrapCollection(parameter), rowBounds); 点进去,是个接口,找实现类 BaseExecutor

执行到 BaseExecutor 的 queryCursor(

@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    return doQueryCursor(ms, parameter, rowBounds, boundSql);
}

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

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

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
        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 是一个接口 Sqlsource 。

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

类中 getBoundSql 是一个核心方法,MyBatis 也是通过这个方法来为我们构建sql。

BoundSql 对象其中保存了经过参数解析,以及判断解析完成sql语句。

比如<if> <choose> <when> 都会在这一层完成,具体的完成方法往下看,Sqlsource 最常用的实现类是DynamicSqlSource。

public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    @Override
    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);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }

}

核心方法是调用了rootSqlNode.apply(context),其中rootSqlNode是一个接口 SqlNode。

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

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

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

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

  @Override
  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 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)).compareTo(BigDecimal.ZERO) != 0;
    }
    return value != null;
}

关键点就在于这里,在OgnlCache.getValue中调用了Ognl.getValue,看到这里恍然大悟,mybatis是使用的OGNL表达式来进行解析的,在OGNL的表达式中,'y' 会被解析成字符,因为java是强类型的,char 和 string 会导致不等,所以 if 标签中的sql不会被解析。

具体的请参照 OGNL 表达式的语法。

到这里,终于知道上面的问题是怎么回事了,只需要把代码修改成:(内双外单

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

也可以把代码修改成 'y'.toString()

<if test="type == 'y'.toString()">  
    and status = 1   
</if>  

这样就解决了,虽然这个问题微不足道,但是有的人遇到了,一时还真的不知道该怎么办,这就提醒了大家一定要注意细节!

0

评论区