【MyBatis源码】SqlSource对象创建流程

文章目录

介绍

代码入口:

java 复制代码
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

languageRegistry:用于注册LanguageDriver,LanguageDriver用于解析SQL配置,将配置信息转换为SqlSource对象。

MyBatis中的SqlSource用于描述SQL资源,MyBatis可以通过两种方式配置SQL信息,一种是通过@Selelect、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider等注解;另一种是通过XML配置文件。SqlSource就代表Java注解或者XML文件配置的SQL资源。

java 复制代码
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

SqlSource接口的定义非常简单,只有一个getBoundSql()方法,该方法返回一个BoundSql实例。BoundSql是对SQL语句及参数信息的封装,它是SqlSource解析后的结果。如图9-1所示,SqlSource接口有4个不同的实现,分别为StaticSqlSource、DynamicSqlSource、RawSqlSource和ProviderSqlSource。

这4种SqlSource实现类的作用如下。

ProviderSqlSource:用于描述通过@Select、@SelectProvider等注解配置的SQL资源信息。DynamicSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句。

RawSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,与DynamicSqlSource不同的是,这些SQL语句在解析XML配置的时候就能确定,即不包含动态SQL相关配置。

StaticSqlSource:用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。

XMLScriptBuilder初始化

java 复制代码
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

在 MyBatis 源码中,initNodeHandlerMap 方法的作用是初始化一个用于处理不同节点类型的映射表(nodeHandlerMap)。每个节点类型(例如 "trim"、"where"、"set" 等)对应一个处理器(例如 TrimHandler、WhereHandler 等)。这些处理器负责解析和处理相应的 SQL 语句节点。

java 复制代码
  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());
  }

• trim:用于去除 SQL 语句两端的空格或特定字符。

• where:用于生成 WHERE 子句。

• set:用于生成 SET 子句,通常在更新操作中使用。

• foreach:用于处理集合中的元素,通常用于生成批量插入或更新的 SQL。

• if:用于根据条件动态生成 SQL 片段。

• choose、when、otherwise:类似于 Java 的 switch-case 语句,用于动态选择生成不同的 SQL 片段。

parseDynamicTags解析动态节点

java 复制代码
  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));
      // 如果子节点是文本节点或 CDATA 节点,提取其文本内容并创建 TextSqlNode 对象
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 如果是动态的,添加到 contents 列表中,并标记为动态;否则,创建一个静态文本节点并添加
        if (textSqlNode.isDynamic()) {
          // 检查这个文本节点是否为动态,即是否包含 ${}动态表达式
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 将带有#{}符号的SQL封装到StaticTextSqlNode节点上
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
        // 如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)
        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;
      }
    }
    // 将所有解析的 SQL 节点封装到 MixedSqlNode 对象中并返回
    return new MixedSqlNode(contents);
  }

以下面这个SQL语句作为范例进行描述解析

java 复制代码
    select * from t_user where id = #{id}
      <choose>
        <when test="name != null">
          AND name like #{name}
        </when>
        <otherwise>
          AND name = #{name}
        </otherwise>
      </choose>

select * from t_user where id = #{id} 文本走的是Node.TEXT_NODE

而对于 都属于Node.ELEMENT_NODE

如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)

java 复制代码
   // 如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)
        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;

解析完各个Node节点后,封装SqlSource

java 复制代码
  public SqlSource parseScriptNode() {
    // 解析动态 SQL 标签,并生成一个 MixedSqlNode 对象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      // 如果是包含${}符号或者包含动态SQL的元素节点
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 纯文本SQL,仅包含#{}
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

RawSqlSource分析

代码分析

RawSqlSource 的作用是将没有动态 SQL 标签的 SQL 语句(例如 、 等)直接解析成静态 SQL,避免了动态解析的开销。它会将 SQL 中的 #{} 占位符参数解析成 StaticSqlSource,然后存储为 BoundSql 对象,以便在执行时直接使用。

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

下面这段代码是 SqlSourceParser 的 parse 方法,它负责将包含 #{} 占位符的 SQL 字符串解析为 StaticSqlSource 对象。StaticSqlSource 表示一个纯静态的 SQL 源,适合不含动态 SQL 标签的场景。以下是对这段源码的详细分析

java 复制代码
 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());
  }

ParameterMappingTokenHandler 是 MyBatis 中一个用于处理 #{} 占位符的处理器。它会根据 parameterType 和 additionalParameters 等信息,将 #{} 占位符替换为相应的 JDBC 参数标记(如 ?),并生成对应的 ParameterMapping 列表,用于描述每个参数的类型和属性。

GenericTokenParser 是 MyBatis 中一个通用的占位符解析器,它能够识别带有指定前后缀的占位符。这里的 parser 会针对 #{} 占位符进行解析,将匹配到的 #{} 内的内容交给 handler 处理。

通过 GenericTokenParser 对 SQL 字符串进行解析,处理 #{} 占位符。

removeExtraWhitespaces(originalSql) 方法会去除多余的空白字符,确保 SQL 语句结构更简洁。

最终的 sql 字符串中,#{} 占位符将被替换为 ?,以便后续参数绑定

解析完成后,parse 方法会返回一个 StaticSqlSource 对象。StaticSqlSource 是 MyBatis 中用于表示静态 SQL 的 SqlSource 实现,适合不包含动态逻辑的 SQL。handler.getParameterMappings() 获取所有参数的映射信息,以便在执行时能够将参数正确绑定到 SQL 的 ?占位符上。

实例化

示例SQL:

java 复制代码
     select * from t_user where id = #{id} name = #{name}

parse 方法的整体流程是:解析 #{} 占位符,将其替换为 ?,并记录参数映射信息。

经过 parse 处理后,原始 SQL 将被替换为:select * from t_user where id = ? name = ?

handler.getParameterMappings() 的作用是返回一个 ParameterMapping 列表,用于描述 SQL 中的 #{} 占位符参数的映射信息。ParameterMapping 对象包含了每个参数的名称、类型、Java 属性等,这些信息在 SQL 执行时用于将实际参数绑定到 ? 占位符上。

ParameterMappingTokenHandler 会生成一个 ParameterMapping 列表,包含两个参数的映射信息。

在 SQL 执行时,MyBatis 会使用 parameterMappings 列表来将 User 对象中的 id 和 name 属性值绑定到 SQL 中的 ? 占位符上。

ParameterMappingTokenHandler 在解析 #{} 占位符时,会将每个参数按出现的顺序记录到 parameterMappings列表中。比如,#{id} 出现在 #{name} 之前,那么 parameterMappings 列表中,id 的映射会排在 name 之前。MyBatis 通过 parameterMappings 中记录的参数顺序、名称等信息,将参数依次正确绑定到 SQL 中的 ? 占位符上,因此在参数传递过程中能确保参数的正确替换位置。

相关推荐
大只因bug1 分钟前
基于Springboot的在线考试与学习交流平台的设计与实现
java·spring boot·后端·学习·mysql·vue·在线考试与学习交流平台系统
想进大厂的小王10 分钟前
Spring Boot⾃动配置
java·spring boot·后端
CL_IN39 分钟前
高效集成:聚水潭奇门至金蝶云星空的数据流自动化
java·前端·自动化
Ylucius41 分钟前
14天速成前端 ------学习日志(已完结)------ 后端程序员学习了解前端
java·开发语言·前端·vue.js·学习·状态模式·1024程序员节
就叫飞六吧1 小时前
关于Java中**optional,stream,lambda**
java·开发语言
调皮的木木1 小时前
zookeeper全系列学习之分布式锁实现
java·分布式·zookeeper
零希1 小时前
正则表达式
java·数据库·mysql
lovelin+v175030409661 小时前
电商平台店铺运营:巧用 API 接口的策略之道
java·大数据·后端·python
ln-x1 小时前
在IDEA中运行Mybatis后发现取出的password值为null
java·intellij-idea·mybatis
琪露诺大湿1 小时前
JavaEE-多线程初阶(1)
java·linux·开发语言·jvm·数据库·java-ee·1024程序员节