MyBatis源码分析之Script用法详解

Posted 叶长风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis源码分析之Script用法详解相关的知识,希望对你有一定的参考价值。

MyBatis源码分析之Script用法详解

在上一篇文章中讲到MyBatis的#paras和$paras用法,在里面提到在解析sql组装成SqlSource对象时,会判断当前sql是否是动态类型,然后里面有一个对sql中是否含有script的判断,这种用法我以前也没有用过,就看到同事写过一回,感觉和xml中的写法差不多,就是一种动态sql,可以有一些if、else之类的条件判断,今天就来了解一下这个用法,顺便看下源码实现。

1. Script用法


与之前相同,讲到这里还是先讲一下

@ResultMap("BaseResultMap")
@Select("<script>" +
            "select * from user " +
            "<where>" +
            "<if test=\\"age != null\\"> age = #age</if>" +
            "</where>" +
            "</script>")
List<User> getUser4(@Param("age") Integer age);

判断传进去的age字段是否为空,为空则查询全部,不为空则查询对应字段值下的记录,这个动态sql其实写的挺麻烦的,还要转义,注意script的结束等等,我觉得还不如xml写起来方便。

调用程序为:

List<User> users = userMapper.getUser4(null);
System.out.println(users);
System.out.println();
List<User> users2 = userMapper.getUser4(25);
System.out.println(users2);

查询的结果和预料中差不多,users中为全部记录,users2中为1条记录。

2. 源码解析

上述中讲述了

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) 
    // issue #3
    if (script.startsWith("<script>")) 
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
     else 
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) 
        return new DynamicSqlSource(configuration, textSqlNode);
       else 
        return new RawSqlSource(configuration, script, parameterType);
      
    
  

可以转到createSqlSource方法看一看。

@Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) 
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  

public SqlSource parseScriptNode() 
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) 
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
     else 
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    
    return sqlSource;
  

在parseScriptNode中进行了具体的sql类型判断,其中如果isDynamic类型为true,则返回DynamicSqlSource,否则返回RawSqlSource,isDynamic在parseDynamicTags方法中进行初始化,进这个方法看看。

protected MixedSqlNode parseDynamicTags(XNode node) 
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) 
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) 
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) 
          contents.add(textSqlNode);
          isDynamic = true;
         else 
          contents.add(new StaticTextSqlNode(data));
        
       else if (child.getNode().getNodeType() == Node.ELEMENT_NODE)  // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) 
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        
        handler.handleNode(child, contents);
        isDynamic = true;
      
    
    return new MixedSqlNode(contents);
  

这个方法用来解析节点,调试一下看下。

此时node节点内容与我们之前查看的sql相同,中间分析的过程非常多,比较复杂,这就不再单独看了,可以直接看这个方法的返回。

这里返回的rootSQLNode中contents对象有两个,一个为固定查询sql,一个为条件sql,最后在这返回DynamicSqlSource对象。

最后我看执行到executor中的query时的代码。

@Override
  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);
 

调试到getBoundSql处,拿到BoundSql查看,两次分别的结果为:

在传入age为null时,返回为不带查询条件的sql。

在age不为null时,此时返回的sql为:

此时就有age查询条件。我们进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();
        
      
    

在调试代码时可知此时获得的boundSql对象已经是组装完成的sql。

此时已经将sql组装完成了,我们继续进SqlSource方法中看看。

@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);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) 
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    
    return boundSql;
  

在调用rootSqlNode.apply方法,在前面分析知这里的SqlNode是MixedSqlNode.

@Override
  public boolean apply(DynamicContext context) 
    for (SqlNode sqlNode : contents) 
      sqlNode.apply(context);
    
    return true;
  

此时的contents对象就是两个,一个为固定sql,一个是where条件,如下图:

第一个sqlNode没啥说的,固定sql调用append方就可以了,如下:

@Override
  public boolean apply(DynamicContext context) 
    context.appendSql(text);
    return true;
  

进入where条件的SQLNode,where条件没有实现apply方法,此处调用的为父类TrimSqlNode类的apply方法,apply方法为:

@Override
  public boolean apply(DynamicContext context) 
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
  

在调用contents.apply(filteredDynamicContext)方法时还要再回到MixSqlNode处,此时调用就是IfSqlNode。

此时的动态sql就是where中的条件了,然后后续filteredDynamicContext.applyAll()中的操作就是给sql加上where前缀,这里就不再分析了,有兴趣的可以自己看看。


Script用法比较简单,我原想原理应该也是和之前差不多,没想到分析进来这么多处理和判断,可以说看得晕头转向,此处先留个记号,以后有空还要再回来仔细分析一下。

以上是关于MyBatis源码分析之Script用法详解的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis源码分析之@ResultMap注解详解

Mybatis源码分析之Select返回数据分析

深入浅出Mybatis系列---配置详解之typeAliases别名(mybatis源码篇)

mybatis 源码分析Interceptor 详解

Vue 2.0 深入源码分析 基础篇 methods属性详解

Vue 2.0 深入源码分析 基础篇 computed 属性详解