Mybatis 处理 sql 语句

Mybatis 处理 sql 语句,以查询为例

本文 mybatis 处理 sql 语句主要分为编译期间处理和执行期间处理
另外 sql 语句又分为 静态 sql 语句 和动态 sql 语句,用参数修饰符来区分。
静态 sql 语句:没有参数或者参数修饰符全部是 #{} 这种,并且没有其他标签
动态 sql 语句:参数其中之一被 ${} 修饰,或者有标签
Mybatis 对动态 sql 的定义

mybatis 初始化完成后 所有处理完的 sql 语句都存在 SqlSource
Configuration —》 MappedStatement–》 SqlSource

编译期间处理

编译期间对语句的处理主要分为:预编译及非预编译,拼接动态 sql 语句。

预编译

预编译主要操作的是静态 sql 语句。将 #{xx} 替换成 ?的占位符。
原 sql 语句

select * from user_bo ur where ur.id = #{ id} and ur.user_name = #{name }

处理完

select * from user_bo ur where ur.id = ? and ur.user_name = ?

非预编译

原 sql 语句

select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }

处理完

select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }

拼接动态 sql 语句

拼接动态 sql 语句 主要是处理 动态 sql 的标签。
原 sql 语句

    select * from user_bo ur
    <where>
      <if test="id!=null">
        and ur.id = #{ id}
      </if>
      <if test="name!=null">
        and ur.user_name = #{name }
      </if>
    </where>

处理完

select * from user_bo ur where ur.id = #{ id} and ur.user_name = #{name }

或者
原 sql 语句

    select * from user_bo ur
    <where>
      <if test="id!=null">
        and ur.id = ${ id}
      </if>
      <if test="name!=null">
        and ur.user_name = #{name }
      </if>
    </where>

处理完

select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }

运行期间处理

运行期间对语句的处理主要分为: sql 参数赋值
在运行期间对参数赋值主要还是封装的原生 JDBC 的操作。

源码解析

节点:不管是静态 sql 语句 还是 动态 sql 语句 解析完后都是一个节点集合,在代码种用 SqlNode 对象代替, 而节点有很多类型 例如 WhereSqlNode,IfSqlNode 等等…

预编译期间 占位符 替换 #{}

能够满足预编译期间占位符替换 #{} 的 静态 sql 语句主要流程为

  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);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

GenericTokenParser 的 parse(…) 方法主要是 通过Java String 的截取替换操作来完成的。
而返回的正是 一个 StaticSqlSource 静态 sql 资源

动态节点解析

以下文为例

  <select id="selectAll03" parameterType="u" resultType="u">
    select * from user_bo ur
    <where>
      <if test="id!=null">
        and ur.id = #{ id}
      </if>
      <if test="name!=null">
        and ur.user_name = #{name }
      </if>
    </where>
  </select>

先看一下流程图 1 解析节点 完成 sql 的预编译(大纲流程)
在这里插入图片描述
从 parseDynamicTags 到 解析select 节点 开始
XMLScriptBuilder 对象里面 script 属性就是节点信息,并且在上一步初始化了很多种不同的 NodeHandler 节点处理器
XMLScriptBuilder-context-body: select * from user_bo ur
script:

<select parameterType="u" id="selectAll03" resultType="u">
    <where>
        <if test="id!=null">
        and ur.id = #{ id}
      </if>
        <if test="name!=null">
        and ur.user_name = #{name }
      </if>
    </where>
</select>

开始解析的源码:

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

langDriver.createSqlSource:

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

builder.parseScriptNode():

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    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));
        }
      } 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);
  }

if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE)

获取 body: select * from user_bo ur 并不是节点
使用文本类型节点修饰

TextSqlNode textSqlNode = new TextSqlNode(data);

是否为动态类型
textSqlNode.isDynamic():

  public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }

判断有没有${xxx} ,所以不是动态的

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

转化为静态文本sql 节点 存放到 List contents 节点集合中

contents.add(new StaticTextSqlNode(data));

继续循环第二层节点
XNode child:

<where>
    <if test="id!=null">
        and ur.id = #{ id}
      </if>
    <if test="name!=null">
        and ur.user_name = #{name }
      </if>
</where>

这是一个节点对象,所以会走nodeType = 1

child.getNode().getNodeType() == Node.ELEMENT_NODE

通过节点名称 选择节点处理器

NodeHandler handler = nodeHandlerMap.get(nodeName);

处理节点

handler.handleNode(child, contents);

这个节点是where,也是最后一个节点,where 里面又有节点
先进入 WhereHandler

在这里插入图片描述
WhereHandler #handleNode

  private class WhereHandler implements NodeHandler {
    public WhereHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      targetContents.add(where);
    }
  }

又递归掉回来啦 处理里面的 if 节点

MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);

会处理 \n 空格,并且会将空格 转化成 静态的文本sql 节点

contents.add(new StaticTextSqlNode(data));

再处理 if 节点 ,第一个 if 节点里面有 and ,正确的 sql 语句中 where 后面跟着的不是 and 。

<if test="id!=null">
        and ur.id = #{ id}
      </if>

在这里插入图片描述
再回到 IfHandler 里面 又回来啦
在这里插入图片描述
if 里面的 and ur.id = #{ id} 并不是节点,所以 会放到 节点的集合里面去,注意这个节点是在 where 节点里面的,where 节点的解析还未完成

在这里插入图片描述
解析完第一个 if 节点 并封装成 一个 MixedSqlNode 对象

new MixedSqlNode(contents);

再回去 IfHandler # handleNode 里面的第二行

  private class IfHandler implements NodeHandler {
    public IfHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

targetContents 目标 Contents 就是刚刚那个节点集合

此时节点里面还未将 第一个 and 去掉

再来处理 where 里面的第二个 if 节点
处理之前还会处理一次 \n 的空格 并且

contents.add(new StaticTextSqlNode(data));

在这里插入图片描述
再调回来 将 and ur.user_name = #{name } 生成一个静态文本节点,放到节点集合,和上面的 if 节点一样,属于 where 的节点集合

contents.add(new StaticTextSqlNode(data));

处理完 第二个 if 节点 再处理一次 \n 空格 生成一个

contents.add(new StaticTextSqlNode(data));

再回到 WhereHandler # handleNode 的第二行

private class WhereHandler implements NodeHandler {
    public WhereHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      targetContents.add(where);
    }
  }

将where 节点信息 封装成一个 WhereSqlNode
WhereSqlNode 继承了 TrimSqlNode 顾名思义
处理空格的节点(TrimHandler)再返回之前解析的代码
在这里插入图片描述

解析完一共有三个节点

select * from user_bo ur
where 节点
空格

where节点 一共5个
在这里插入图片描述
最后封装成动态 sql 对象 DynamicSqlSource 保存到 MappedStatement。

运行期间参数赋值

运行期间为 ${xxx} 赋值

运行期间为 ${xxx} 赋值 时需要参数类型与数据库字段类型匹配
既然是为 ${xxx} 赋值,那么肯定是一段动态 sql ,但是动态 sql 也可能不包含 ${xxx} 符号修饰的参数,文中的案例使用 id=“selectAll03” 的例子。
PS:如果此动态 sql 没有 ${xxx} 符号修饰,还是会处理此 sql 。
比如案例中的 处理完 后

select * from user_bo ur where ur.id = #{ id} and ur.name = xxx
where 是怎么动态添加的,第一个 and 又是怎么去掉的

  <select id="selectAll03" parameterType="u" resultType="u">
    select * from user_bo ur
    <where>
      <if test="id!=null">
        and ur.id = #{ id}
      </if>
      <if test="name!=null">
        and ur.user_name = ${name }
      </if>
    </where>
  </select>

源码:

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

executor.query:

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

ms.getBoundSql(parameterObject):

BoundSql boundSql = sqlSource.getBoundSql(parameterObject) 有删减

  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 类型为 MixedSqlNode
在这里插入图片描述
在这里插入图片描述
三种策略都会执行

第一种 StaticTextSqlNode
在这里插入图片描述

第二种 WhereSqlNode 这个节点里面又有五种,上面解析过的
在这里插入图片描述
回来循环第二种 WhereSqlNode
在这里插入图片描述
第二种的 第一个,第三个,第五个 StaticTextSqlNode 都是处理 \n 空格
我们先看第一个 ifSqlNode ,先 ifSqlNode 再跳转 MixedSqlNode
在这里插入图片描述
再跳转 StaticTextSqlNode 处理空格
在这里插入图片描述
在这里插入图片描述
处理第二个 IfSqlNode
在这里插入图片描述
在这里插入图片描述
这一段里面有一个 ${xxx} 修饰的参数,会进入到参数赋值

  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

parse(text):部分代码

builder.append(handler.handleToken(expression.toString()));

然后通过类型进行赋值
在这里插入图片描述
赋值完成之后再回到 那个 for 循环里面 再处理 where 标签内返回的结果
在这里插入图片描述

先全部 大写 AND UR.ID = #{ ID} AND UR.USER_NAME = AAA

    public void applyAll() {
      sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
      String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
      if (trimmedUppercaseSql.length() > 0) {
        applyPrefix(sqlBuffer, trimmedUppercaseSql);
        applySuffix(sqlBuffer, trimmedUppercaseSql);
      }
      delegate.appendSql(sqlBuffer.toString());
    }

去掉第一个 and applyPrefix(sqlBuffer, trimmedUppercaseSql);
在这里插入图片描述
TrimSqlNode 在创建构造方法的时候将 WHERE 传进来啦
将 where 放在 最前面
在这里插入图片描述
当前的 sql 为 WHERE ur.id = #{ id} and ur.user_name = aaa

处理后缀 applySuffix(sqlBuffer, trimmedUppercaseSql);

处理完 ${xxx} 后再返回 处理 #{xxx} 变成 ?占位符

  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;
  }

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;
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

占位符 完成替换后 再返回到 具体的查询方法

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

创建缓存 key 进行查询方法 ,先根据key 从二级缓存中去拿,再从一级缓存中去拿,最后去查询

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

由四大对象中的 ParameterHandler 来处理参数赋值操作

运行期间为 为占位符赋值

stmt = prepareStatement(handler, ms.getStatementLog());

在这里插入图片描述
在这里插入图片描述
原生 JDBC 完成 预编译工作
在这里插入图片描述
再回来行进参数赋值
在这里插入图片描述

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

开始赋值,先获取参数类型,在通过策略模式 使用该参数类型对象完成参数赋值,也是封装的 JDBC
在这里插入图片描述
通过基类路由进来,完成具体的赋值操作
在这里插入图片描述
再看一下流程图 2 解析 sql 的 SqlSource

在这里插入图片描述
再看一下流程图 3 执行 sql 语句 完成 SqlSource 的预编译及${xxx} 的赋值
在这里插入图片描述
最终返回到 query 方法交给子类去doQuery

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值