MyBatis核心处理层:MyBatis初始化流程补充(动态sql解析)

1:概述

       在写完MyBaits核心处理层时,总感觉对于动态sql语句的解析没有写清楚,所以对于SqlNode,SqlSource这两个类相关的东西再写一篇博客,也算是对MyBatis源码的相关知识点的一次回顾,童鞋在看完这篇的时候,可以再回顾一下MyBatis初始化流程下,可能对sql语句解析的章节理解起来更容易。其实SqlNode和SqlSource这两个类,我个人感觉不仅仅是MyBaits动态sql的实质表现,更是让我们过渡,剥离XML相关内容的桥梁,就好比SQLNode,SqlSorce左边是XML,而右边纯粹是相关Java语言内容,非xml相关的东西。同时SqlNode和SqlSource对于后面的MyBatis执行流程的理解也是非常重要的。
       在分析SqlNode相关知识之前,如果对Ognl不熟悉的同学,可参考下面这篇文章:https://www.cnblogs.com/cenyu/p/6233942.html 自行学习一下,因为SqlNode在解析一些表达式值的时候会用到Ongl这个工具。

2:SqlNode相关

       还是老规矩,先看SqlNode继承体系的UML类图。如下所示:
在这里插入图片描述
       看到上面的UML。童鞋是不是觉得这些实现类跟我们平时写的动态sql里面的标签非常的相似,是的,在看过我们MyBatis初始化流程下的童鞋应该还记得,像if标签对应的就是我们的IfSqlNode,forEache标签对应的就是我们的ForEachSqlNode。下面我们来一一介绍每个实现类的功能以及创建,
        在介绍实现类之前,我们先看一下SqlNode接口的定义,如下,为SqlNode的代码定义:
在这里插入图片描述
       接口定义很简单,只有一个apply方法,apply方法的功能也只是解析动态sql标签的,至于怎么解析,我们在下面的实现类分析。

2.1 TextSqlNode

       TestSqLNode表示的是映射文件里面sql语句的文本节点。它里面提供给了一个很重要的方法是isDynamic,代码定义如下:在这里插入图片描述
代码分析:

  • 代码(1)处,创建TolenHandler实现类,该知识点在MyBatis的基础模块的解析模块提到过。
  • 代码(2)处,创建TokenParse,指定openToken为${,closeToken为}。TokenParse的知识点也在基础模块的解析模块提到过。
  • 代码(3)处,调用TokenParse的parse方法查找文本是否包含占位符,若有,则Dynamic置为true。
  • 代码(4)处,根据代码(3)处的执行结果判断是否是动态Sql.

注:text属性为创建TextNode对象是传进来的文本标签里面的文本。

2.1.2 TextSqlNode#apply()方法

       TextSqlNod的apply方法实现代码如下,总的逻辑比较简单,主要是要对TokenParse这个接口要了解,童鞋可以先不关注DynamicContext这个接口是干啥的,只需要知道,他里面一定有我们在调用的时候传进来的实参。
在这里插入图片描述
代码分析:

  • 代码(1)处,创建TokenParse,指定openToken为“${”,closeToken为“}”。
  • 代码(2)处,调用context里面的sqlBuilder方法将解析后的字符串添加到Context里面去。

2.2: IfSqlNode

        IfSqlNode对应的是if标签,如下为IfSqlNode对应的代码定义:
在这里插入图片描述
apply方法代码分析:

  • 代码(1)处。判断条件表达式是否满足。此处使用了Ognl表达式。
  • 代码(2)处。调用if标签的子标签的apply方法。

        注:对于if标签的子标签,通常来说,一般为textSqlNode。ifSqlNode标签的构造 其实在MyBatis初始化流程里面有讲到,童鞋可参考XMLScriptBuilder里面的内部类IfHandler构建ifSqlNode的代码。

2.3:TrimSqlNode

       TrimSqlNode对应的标签是Trim,TrimSqlNode会根据子节点的解析结果,相应的删除添加前缀或者后缀。TrimSqlNode相关属性的定义如下:
在这里插入图片描述
       如下,为trimSqlNode的apply方法代码定义:
在这里插入图片描述
代码分析:

  • 代码(1)处,构建FilteredDynamicContext,该类是TrimSqlNode的内部类,实质也是DynamicContext的一个装饰器(装饰器模式在MyBatis里很常见,缓存模块就是典型的用法),FilteredDynamicContext仅仅对appendSql扩展了一下,代码不难,童鞋可以看一下。
  • 代码(2)处,处理子标签的结果,因为SqlNode的构建形式本身就是一种树形结构,这个在MyBatis的初始化流程就谈到过。此处,童鞋可以简单假设一下,子标签就是if标签,对应的也就是调用IfSqlNode的apply方法。
  • 代码(3)处,前后缀处理,这个就是处理FilteredDynamicContext的sqlBuffer的前缀或者后缀,然后调用context的appendSql将处理之后的结果添加进去。
            不知道童鞋有没有思考过,此处,创建DynamicContext的装饰器,然后就扩展了appendSql方法,其余的都是调用delegate也就是context相关方法,个人感觉,这块MyBatis还是思考的非常周到的,因为通过创建这么一个装饰器类,让trim标签的前缀后缀的作用只能作用到trim的子标签,并不会污染到其它已经解析好的sql语句,因为在调用TrimSqlNode方法之前,可能已经调用了其它SqlNode的apply方法,context里面的sqlBuilder已经有值,如果没有这个装饰器的话,会污染已经解析出来的sql,这个小细节,童鞋可仔细揣摩揣摩。对于一些复杂的TrimSqlNode的构建童鞋可参考XMLScriptBuilder的内部类TrimHandler,这个也在MyBatis初始化流程下篇提到过。

2.4:WhereSqlNode 和SetSqlNode

        WhereSqlNode 对应的标签是where标签,继承自TrimSqlNode,不同的是,WhereSqlNode的prefix属性固定为WHERE,prefixesToOverride为固定集合,如下:为WhereSqlNode代码定义:

public class WhereSqlNode extends TrimSqlNode {

    private static List <String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

    public WhereSqlNode(Configuration configuration, SqlNode contents) {
        super(configuration, contents, "WHERE", prefixList, null, null);
    }

}

        SetSqlNode 对应的标签是set标签,同样也是继承TrimSqlNode,不同的是,SetSqlNode的prefix固定为 set ,prefixesToOverride,suffixesToOverride固定为逗号,如下,为对应的SetSqlNode的代码定义:

public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}

        对于WhereSqlNode和SetSqlNode,其实就是TrimSqlNode的两种特殊表现形式,两者的apply方法就不再讲述了,逻辑同TrimSqlNode,童鞋们也可以结合自己平时写的动态sql,回顾一下,是不是其实它们的达到的功能是一样的。对于两者的构建,童鞋们可参考XMLScriptBuilder里面的内部类WhereHandler和SetHandler相关代码。

2.5 ForEachSqlNode

        ForEachSqlNode对应的标签是foreach标签,在使用该标签迭代元素时,不仅可以使用集合的袁术和索引值,还可以在循环开始前,循环结束后添加指定的字符串,而且也允许在迭代过程中添加指定的分隔符。结合这些功能,我们先来看ForEachSqlNode定义了哪些属性,如下,为ForEachSqlNode的属性,含义我已经打上了注释,童鞋们可参考看一下:
在这里插入图片描述
        在了解了ForEachSqlNode相关属性之后,我们再看看ForEachHandler是怎么构建ForEachSqlNode,这块其实在MyBatis初始化流程下篇的sql语句解析已经提到,如下为XMLScriptBuilder的内部类ForEachHandler,代码比较简单,童鞋可参开代码注释自己了解:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
        // Prevent Synthetic Access
    }
    @Override
    public void handleNode(XNode nodeToHandle, List <SqlNode> targetContents) {
        //获取foeEach标签的子节点
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        //获取简单属性
        String collection = nodeToHandle.getStringAttribute("collection");
        String item = nodeToHandle.getStringAttribute("item");
        String index = nodeToHandle.getStringAttribute("index");
        String open = nodeToHandle.getStringAttribute("open");
        String close = nodeToHandle.getStringAttribute("close");
        String separator = nodeToHandle.getStringAttribute("separator");
        //构建
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
        targetContents.add(forEachSqlNode);
    }
}

        了解了ForEachSqlNode的的内部属性和构建流程之后,我们再来看看它的apply方法,如下,为ForEachSqlNode的apply方法代码:

public boolean apply(DynamicContext context) {
    Map <String, Object> bindings = context.getBindings();
    //(1):通过ognl并结合实参获得collection表达式的的迭代值
    final Iterable <?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
        return true;
    }
    boolean first = true;
    //(2):应用Open属性
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
        DynamicContext oldContext = context;
        if (first || separator == null) {
            context = new PrefixedContext(context, "");
        } else {
            context = new PrefixedContext(context, separator);
        }
        //解析到第几个子节点
        int uniqueNumber = context.getUniqueNumber();
        //(3):绑定本次循环参数
        if (o instanceof Map.Entry) {
            @SuppressWarnings("unchecked")
            Map.Entry <Object, Object> mapEntry = (Map.Entry <Object, Object>) o;
            applyIndex(context, mapEntry.getKey(), uniqueNumber);
            applyItem(context, mapEntry.getValue(), uniqueNumber);
        } else {
            //设置参数值,params[index]=i,params[_frch_index_1..10]=i,
            applyIndex(context, i, uniqueNumber);
            //设置参数值,params[item] = o,params[_frch_item_1..10]=0
            applyItem(context, o, uniqueNumber);
        }
        //(4):调用子节点的解析参数
        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
        if (first) {
            first = !((PrefixedContext) context).isPrefixApplied();
        }
        context = oldContext;
        i++;
    }
    //(5):应用close方法
    applyClose(context);
    //(6):删除循环的参数
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
}

逻辑稍微有点绕,童鞋可结合代码分析,代码注释和测试用例一起分析:
代码分析:

  • 代码(1)处,通过ognl并集合参数获得collection表达式的迭代变量。此处,需要了解ognl表达式,童鞋可集合本文开片推荐的博客进行学习。
  • 代码(2)处,应用open属性,比较简单,就不再阐述。
  • 代码(3)处,设置每次循环的参数,此处,会对参数类型进行判别,如果是集合类型,将新增四个参数,params[index]=i,params[_frch_index_1…10]=i,params[item]=o,params[_frch_item_1…10]=0,
  • 代码(4)处,设置好每次循环的参数之后,调用子节点的apply方法。此处,FilteredDynamicContext需要特别留意appenSql方法,会对“#{}”占位符进行特殊处理,

测试用例:

private DynamicSqlSource createDynamicSqlSource(SqlNode... contents) throws IOException, SQLException {
    //MyBatis配置文件
    final String resource = "MapperConfig.xml";
    final Reader reader = Resources.getResourceAsReader(resource);
    SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
    Configuration configuration = sqlMapper.getConfiguration();
    MixedSqlNode sqlNode = mixedContents(contents);
    return new DynamicSqlSource(configuration, sqlNode);
   
}

@Test
void shouldSkipForEachWhenCollectionIsEmpty() throws Exception {
    final HashMap <String, Integer[]> parameterObject = new HashMap <String, Integer[]>() {{
        put("array", new Integer[]{1,2});
    }};
    final String expected = "SELECT * FROM BLOG";
    DynamicSqlSource source = createDynamicSqlSource(new TextSqlNode("SELECT * FROM BLOG"),
        new ForEachSqlNode(new Configuration(), mixedContents(
            new TextSqlNode("#{item}")), "array", "index", "item", "WHERE id in (", ")", ","));
    BoundSql boundSql = source.getBoundSql(parameterObject);
    assertEquals(expected, boundSql.getSql());
    assertEquals(0, boundSql.getParameterMappings().size());
}

第一次循环的调用中间参数:
在这里插入图片描述
结果:
在这里插入图片描述

2.6 ChooseSqlNode

       ChooseSqlNode对应于chose标签,类似Java语言标签的swtich功能,如下,为ChooseSqlNode的代码定义:
在这里插入图片描述
       ChooseSqlNode的apply方法逻辑比较简单,首先遍历 ifSqlNodes 集合并调用其中SqlNode对象的 apply()方法,然后根据前面的处理结果决定是否调用 defaultSqlNode 的 apply()方法。此处逻辑,主要是要明白它对应的两个属性defaultSqlNode和ifSqlNodes表示的是什么意思,注释已在上面代码写明,至于这两个属性怎么解析出来的,可参开XMLScriptBuilder内部类ChooseHandler的handleNode方法。

2.7 VarDeclSqlNode

       VarDeclSqlNode对应bind标签。代码比较简单,实现方法就不再粘贴出来进行详细分析了。

3.sqlSource相关

       SqlSource相关类的UML如下图所示:
在这里插入图片描述
        对于DynamicSqlSource和RawSqlSource这两个类,童鞋在看完初始话流程下篇的时候应该看见过它的身影,在那里,曾介绍过它们,说RawSqlSource对应的是非静态sql,而DynamicSqlSource对应的是动态sql。这里,我们再集合上篇文章,一起分析SqlSource所有实现类的区别。
       在分析各个实现类之前,我们先来看看SqlSource的代码定义:
在这里插入图片描述
       SqlSource的定义比较简单,只有一个getBoundSql方法,该方法主要是完成动态sql语句解析的入口。

3.1 DynamicSqlSource

       DynamicSqlSource是比较常用的SqlSource,实现逻辑比较简单,如下,为对应的getBoundSql代码实现:

 @Override
 public BoundSql getBoundSql(Object parameterObject) {
     DynamicContext context = new DynamicContext(configuration, parameterObject);
     //(1):解析动态sql
     rootSqlNode.apply(context);
     SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
     //(2):获取参数类型
     Class <?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
     //(3):解析sql
     SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
     //(4):调用StaticSqlSource的getBoundSql
     BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
     //(5):将context里面的参数赋值到boundSql里面去。
     context.getBindings().forEach(boundSql::setAdditionalParameter);
     return boundSql;
 }

代码分析:

  • 代码(1)处,解析动态sql,该处在前面已经说过,主要是调用sqlNode的子类实现动态sql的解析,通过该处,已得到了根据参数解析${}占位符的sql语句,基本sql骨架已完成,其实只剩下#{}占位符没有解析。
  • 代码(2)处,获取实参类型。
  • 代码(3)处,解析sql,该处需要了解SqlSourceBuilder这个类,主要是通过sqlSourceBuilder的parse方法完成解析的,参考3.1.2分析。此处返回的是StaticSqlSource这个类。
  • 代码(4)处 调用StaticSqlSource返回BoundSql,StaticSqlSource的相关功能见3.3相关的分析。
  • 代码(5)处,将context里面的参数赋值到boundSql里面去,代码比较简单,不再阐述,

3.1.2 SqlSourceBuilder

       SqlSourceBuilder 主要完成了两方面的操作, 一方面是解析 SQL 语句中的“#{}”占位符 中定义的属性,格式类似于#{_frc_item_0, javaType= int, jdbcType=NUMERIC,typeHandler=MyTypeHandler},另一方面是将 SQL 语句中的#{}占位符替换成“?”,同样,SqlSouceBuilder也是继承于BaseBuilder,其核心方法parse代码定义如下:

public SqlSource parse(String originalSql, Class <?> parameterType, Map <String, Object> additionalParameters) {
    //(1):构造token实现类
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //(2):构造parse实现类
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //(3):解析
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

代码分析:

  • 代码(1)处,构建Token实现类,到了这里,童鞋们应该对这个类很清楚了,这个解析类在我们的基础层解析器模块就已经提到过了
  • 代码(2)处,同样,构建我们的parse类,这个类在解析器模块里也提到过,就不再阐述了。
  • 代码(3)处,执行解析逻辑,对于ParameterMappingTokenHandler的handleToken方法,建议感兴趣的童鞋可以看一下,里面可能理解比较难。此处,需要特别注意的是,会生成ParameterMapping这个类的集合,ParameterMapping这个类比较简单,童鞋大概了解一下相关属性就行,总之,这个解析的地方完成了两个功能,一个是生成ParameterMapping这个,另外一个功能是将#{}替换成了?。

3.2 RawSqlSource

       在MyBatis初始化下篇提到过,RawSqlSource是静态sql语句,童鞋们在看了MyBiats初始化下篇应该知道,MyBatis判断是否是动态sql语句的标准是文本是否有${}这个占位符。所以,此处我们可以想一下,RawSqlSource的getBoundSql方法的实现逻辑是否同DynamicSqlSouce的getBoundSql方法在执行完SqlNode的applyNode方法之后有相似之处。如下,为RawSqlSource的代码定义:

public class RawSqlSource implements SqlSource {
    private final SqlSource sqlSource;
    public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class <?> parameterType) {
        this(configuration, getSql(configuration, rootSqlNode), parameterType);
    }
    public RawSqlSource(Configuration configuration, String sql, Class <?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class <?> clazz = parameterType == null ? Object.class : parameterType;
        //1:通过SqlSourceBuilder构建sqlSource,返回的是StaticSqlSource
        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap <>());
    }
    private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
        DynamicContext context = new DynamicContext(configuration, null);
        rootSqlNode.apply(context);
        return context.getSql();
    }
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return sqlSource.getBoundSql(parameterObject);
    }
}

        代码非常简单,在构造函数里通过调用SqlSourceBuilder这个类进行解析sql,该处逻辑通DynamicSqlSouce方法非常相似,在前面已经分析过,此处就再阐述了,对于Apply方法,也是调用StaticSqlSource的getBoundSql方法,详细分析见3.3

3.3 StaticSqlSource

        StaticSqlSource在上面分析DynamicSqlSouce和RawSqlSource就已经提到,下面我们直接看它的代码定义:
在这里插入图片描述
        代码没有什么多复杂,童鞋只需要知道各个属性的含义就可以,它的getBoundSql方法也比较简单,只是直接new一个BoundSql。

4总结

        到这里,我们对SqlNode和SqlSource接口相关类已全部做了说明,SqlNode的apply方法主要是根据入参,解析动态sql,生成基本sql语句骨架,并且解析${}占位符的值,而SqlSouce的getBoundSql是根据SqlNode的解析后的结果,再次处理#{}占位符。本篇文章主要为了更好了解MyBatis初始化流程下篇做扩展的(文章连接),本篇有助于了解MyBatis是如何解析映射文件的。对于有些知识点的分析,博主可能总结分析不当之处,各位大佬可留下评论一起探讨探讨,感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值