Mybatis源码分析十一之SqlSource

一、SqlSource

分析mapper映射文件解析时,我们只是整体介绍了mapper映射文件,而sql语句的解析过程没做进一步说明,分析中可知,用户编写的sql语句是存放在SqlSource对象中,但是语句中的参数怎么解析的、参数映射关系是怎么保存的以及动态sql是怎么实现的,将是本文的重点。

SqlSource是只有一个方法的简单接口。

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

它有如下实现类:
在这里插入图片描述
从实现类看,它包含着动态的、静态的、默认的、注解类型等不同的实现方式,回到创建SqlSource入口位置:

//context是当前语句标签节点对象,parameterTypeClass是参数类型
//其中langDriver是XMLLanguageDriver的实例对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

XMLLanguageDriver中的createSqlSource()方法:

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

所以sql信息的解析是通过XMLScriptBuilder 对象实现,继续查看parseScriptNode()方法。

  public SqlSource parseScriptNode() {
  //得到一个混合类型的sql节点
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
    //创建动态sql
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
    //创建原生类型的sql
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

怎么判断是动态还是原生呢?我们需要从parseDynamicTags(context)方法中寻找答案。

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //先获取所有的子节点
    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));
        }
        //如果当前的节点是元素节点,也就是是一个标签节点,此时也是一种动态sql
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        //获取节点处理器,也就是子节点是trim、where、set、foreach、if、choose、when、otherwise、bind
        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);
  }

所以从分析来看,当语句中包含${}表达式、或者包含trim、where、set、foreach、if、choose、when、otherwise、bind子节点,那么当前的就是一个动态sql,也就是返回的对象是DynamicSqlSource,相反语句中只有#{}表达式,说明只是一个简单的原生类型sql。

二、BoundSql

第一部分把sql语句解析成一个DynamicSqlSource或RawSqlSource对象,此时都还是一个原生的文本类型数据,还不能直接使用,此时需要解析成直接可用的BoundSql对象。

BoundSql对象,从作者的注释来看,是从SqlSource处理完所有动态内容之后得到。我们先看看RawSqlSource实例是怎么处理的,查看其getBoundSql方法:

  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

查看其构造方法可知,sqlSource是通过SqlSourceBuilder解析而来。

//其中sql又是通过getSql方法来获取
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());

getSql():

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    //此时会调用MixedSqlNode中的apply方法,该方法就是循环的执行contents中SqlNode的apply方法。
    rootSqlNode.apply(context);
    return context.getSql();
  }

至于原生类型sql,我们只需要看StaticTextSqlNode中的apply方法。

  @Override
  public boolean apply(DynamicContext context) {
  //把当前的语句添加到StringJoiner中(分隔符是空格)
    context.appendSql(text);
    return true;
  }

也就是当前的静态sql语句会保存在StringJoiner中,继续回到SqlSourceBuilder的parse方法。

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  //参数映射处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    //如果配置了shrinkWhitespacesInSql=true就会处理多余的空格先,默认是false
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
    //解析成可以执行的sql
      sql = parser.parse(originalSql);
    }
    //返回一个静态的SqlSource实例
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

parse方法会把当前语句中#{}表达解析成"?",handler 会根据表达式中的内容,跟参数类型进行匹配,然后生成参数映射关系(List< ParameterMapping>)。具体可以查看handler的handleToken。

此时我们再看StaticSqlSource的getBoundSql方法。

  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

会直接返回已经解析好的sql语句、参数映射关系、以及参数类型。

接下来看看DynamicSqlSource类型的处理过程。

  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //会根据不同的标签where、if、set等,分别进行解析
    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);
    //配置额外参数,如果传递的是单个参数,并且是基本类型,可以通过任意表达式获取到值,如果是多个参数,比如int id1,int id2 那么可以通过arg0、arg1或者param1、param2来获取(以此类推)
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

三、总结

本文简单分析了sql语句信息的解析过程,主要分析了动态和静态SqlSource,其实都是按照你的配置信息去一点点解析成对应的Java对象,最后反应到BoundSql 这个结果对象上。

注意:在写表达式的时候会有#{}和${},前者会对表达式内容用"?"代替,防止sql注入问题,后者会根据传递的值,进行直接赋值,这样会提升sql注入的风险,所以需尽量避免使用 ${}表达式,如果存在动态分组或者动态排序时则需使用 ${}方式

以上,有任何不对的地方,请指正,敬请谅解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
引用中提到,Mybatis是一个可以使用简单的XML或者注解来配置和映射原生信息的框架,它可以将接口和Java的POJO映射成数据库中的记录。同时,Mybatis支持定制化SQL、存储过程以及高级映射,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的繁琐操作。 要进行Mybatis源码分析,需要深入研究Mybatis的核心组件和原理。其中,SqlSessionFactoryBuilder用于构建SqlSessionFactory,SqlSessionFactory负责创建SqlSession,SqlSession是与数据库交互的主要接口,通过SqlSession可以执行SQL语句并获取结果。在SqlSession的底层,涉及到Executor、StatementHandler、ParameterHandler和ResultSetHandler等核心组件。 Executor负责执行SQL语句,StatementHandler负责处理SQL语句的预编译和参数设置,ParameterHandler负责处理SQL语句的参数传递,ResultSetHandler负责处理SQL语句的结果集。Mybatis通过这些核心组件的协作来完成数据库操作。 在具体操作时,MybatisSQL映射文件(或注解)中定义了SQL语句和参数映射关系,Mybatis会根据配置的Mapper接口和对应的SQL语句,动态生成Mapper接口的实现类。通过动态代理的方式,实现了Mapper接口的方法与SQL语句的绑定,使得开发者可以直接调用Mapper接口的方法来执行SQL语句。 总之,Mybatis源码分析需要深入了解其核心组件和原理,包括SqlSessionFactory、SqlSession、Executor、StatementHandler、ParameterHandler和ResultSetHandler等。通过分析这些组件的工作原理和协作关系,可以更好地理解Mybatis的内部实现机制。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Mybatis源码分析](https://blog.csdn.net/zyyforever/article/details/101289858)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟+1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值