Mybatis源码解析(四):sql语句及#{}、${}的解析

Mybatis源码系列文章

手写源码(了解源码整体流程及重要组件)

Mybatis源码解析(一):环境搭建

Mybatis源码解析(二):全局配置文件的解析

Mybatis源码解析(三):映射配置文件的解析

Mybatis源码解析(四):sql语句及#{}、${}的解析

Mybatis源码解析(五):SqlSession会话的创建

Mybatis源码解析(六):缓存执行器操作流程

Mybatis源码解析(七):查询数据库主流程

Mybatis源码解析(八):Mapper代理原理

Mybatis源码解析(九):插件机制

Mybatis源码解析(十):一级缓存和二级缓存



前言

  • 本文主要讲解<select><insert><update><delete>标签内将sql语句替换#{}和${}为?、并保存下来属性值、入参对象(字符串|对象|Map)组装成BoundSql,后续执行JDBC操作需要的值从此对象中获取
  • 承接上一篇文章Mybatis源码解析(三):映射配置文件的解析,映射配置文件会解析成MappedStatement对象,而BoundSql只是其中一部分
  • 如下认识下BoundSql类
    在这里插入图片描述

一、解析入口

  • 这里说明一下:获取BoundSql对象通过SqlSource的getBoundSql方法获取,构建SqlSource则是创建BoundSql
  • configuration:全局配置文件,我们最终创建的对象SqlSource要挂在MappedStatement对象(每一个实体映射配置文件)下,然后MappedStatement又要挂在Configuration下面
  • context:<select>标签的属性及sql语句组成的XNode对象
  • parameterTypeClass:入参对象的Class
...

// *******创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息
// 问题:sql占位符如何进行的替换?动态sql如何进行的解析?
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

...
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

在这里插入图片描述

进入createSqlSource创建方法

  • 与解析全局配置文件和映射配置文件套路一样,都是先创建一个xxxBuilder对象,然后再调用解析方法
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 初始化了动态SQL标签处理器
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 解析动态SQL
  return builder.parseScriptNode();
}

进入XMLScriptBuilder对象构造方法

  • 这里主要作用加载动态sql标签的处理器,后面创建sql(带?)时,如果是动态sql则需要通过对应处理器动态构建sql
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
  super(configuration);
  this.context = context;
  this.parameterType = parameterType;
  // 初始化动态SQL中的节点处理器集合
  initNodeHandlerMap();
}

private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}

二、解析SQL

  • 以上准备工作已完成,接下来开始解析工作
  • 回到createSqlSource创建方法,进入builder.parseScriptNode();解析方法
  • parseDynamicTags:解析动态标签
  • RawSqlSource和DynamicSqlSource都是在创建BoundSql的构建方法getBoundSql
public SqlSource parseScriptNode() {
  // ****将带有${}号的SQL信息封装到TextSqlNode
  // ****将带有#{}号的SQL信息封装到StaticTextSqlNode
  // ****将动态SQL标签中的SQL信息分别封装到不同的SqlNode中
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  // 如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    // 如果SQL中包含#{},则将SqlNode封装到RawSqlSource中,并指定parameterType
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

1、解析动态标签

parseDynamicTags方法流程图,下面代码结合着流程图看

在这里插入图片描述

进入parseDynamicTags解析动态标签方法

  • 方法总结:解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
    • 将带有${}号的SQL信息封装到TextSqlNode
    • 将带有#{}号的SQL信息封装到StaticTextSqlNode
    • 将动态SQL标签中的SQL信息分别封装到不同的SqlNode
  • 如下图:一个<select>标签分两部分,文本节点和元素节点;XNode node传递过来的正是此标签解析的对象
    在这里插入图片描述
  • SQL语句中带有${}的话,就是表达式文本
  • isDynamic:是否动态sql标志
    • #{}:带?的sql可以直接生成(因为不会变化),调用时候直接获取
    • ${}或动态sql:每次传入参数不同,带?的sql会变,调用时候生成
  • nodeHandlerMap是上面XMLScriptBuilder构造方法里初始化的动态sql标签的处理器map集合;然后将对应标签的SqlNode对象添加到contents中,如:ChooseSqlNode、IfSqlNode
protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  //获取<select>\<insert>等4个标签的子节点,子节点包括元素节点和文本节点
  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("");
      // 将文本内容封装到SqlNode中
      TextSqlNode textSqlNode = new TextSqlNode(data);
      // SQL语句中带有${}的话,就表示是dynamic的
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        // SQL语句中(除了${}和下面的动态SQL标签),就表示是static的
        // StaticTextSqlNode的apply只是进行字符串的追加操作
        contents.add(new StaticTextSqlNode(data));
      }
      //处理元素节点
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
      String nodeName = child.getNode().getNodeName();
      // 动态SQL标签处理器
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      handler.handleNode(child, contents);
      // 动态SQL标签是dynamic的
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

2、带#{}SQL构建BoundSql

进入RawSqlSource构造方法

  • getSql方法:从SqlNode对象中获取String类型sql语句(目前还是不带?的#{}状态)
  • SqlSourceBuilder构造方法则是参数赋值,主要看下面的parse解析方法
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
  //先调用 getSql(configuration, rootSqlNode)获取sql,再走下面的构造函数
  this(configuration, getSql(configuration, rootSqlNode), parameterType);
}

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
  // 解析SQL语句
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  // 获取入参类型
  Class<?> clazz = parameterType == null ? Object.class : parameterType;
  // 开始解析
  sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}

进入parse解析方法

  • 这里先是创建解析器,并将#{}符号确定下来,真正的解析内容在parser.parse中
  • StaticSqlSource:最后获取BoundSql通过此对象getBoundSql方法
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;
  if (configuration.isShrinkWhitespacesInSql()) {
    sql = parser.parse(removeExtraWhitespaces(originalSql));
  } else {
    // 解析#{}
    sql = parser.parse(originalSql);
  }
  // 将解析之后的SQL信息,封装到StaticSqlSource对象中
  // SQL字符串是带有?号的字符串,?相关的参数信息,封装到ParameterMapping集合中
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

1)parser.parse解析

  • 传入参数originalSql是带#{}的sql,content则是把“#{”和“}"去掉的属性值
  • 这里只截取了核心代码,主要有两个作用
    • 将sql语句带#{属性值}解析成sql带?
    • 将属性值添加到List<ParameterMapping>集合中;ParameterMapping:属性值、jdbc类型等
builder.append(handler.handleToken(expression.toString()));
public String handleToken(String content) {
  parameterMappings.add(buildParameterMapping(content));
  return "?";
}

2)查看StaticSqlSource类

  • 创建完StaticSqlSource对象,则带?的sql语句,及#{}中的参数值已准备就绪,getBoundSql则是拿来创建对象返回即可
  • 这也是上面所说的,不是动态sql:带?的sql是固定的,直接生成

在这里插入图片描述

3、带${}或动态SQL构建BoundSql

查看DynamicSqlSource类

  • 创建完DynamicSqlSource对象,只是赋值对应参数,无其他动作
  • 这也是上面所说的,动态sql:带?的sql是随参数会变化,只在获取时候生成
  • ${}替换?与#{}一样原理,动态sql则是通过判断参数利用动态sql标签处理器叠加的处理修改sql

在这里插入图片描述


总结

  • 本文的核心就是创建BoundSql对象,后续需要带?的sql、#{}里属性值和入参对象则通过SqlSource对象的getBoundSql获取
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冬天vs不冷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值