当前位置: 首页 > article >正文

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

文章目录

    • 介绍
    • XMLScriptBuilder初始化
    • parseDynamicTags解析动态节点
    • RawSqlSource分析
      • 代码分析
      • 实例化

介绍

代码入口:

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资源。

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初始化

  @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 语句节点。

  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解析动态节点

  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语句作为范例进行描述解析

    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)

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

  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 对象,以便在执行时直接使用。

  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 标签的场景。以下是对这段源码的详细分析

 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:

     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 中的 ? 占位符上,因此在参数传递过程中能确保参数的正确替换位置。


http://www.kler.cn/a/377660.html

相关文章:

  • Python毕业设计选题:基于django+vue的4S店客户管理系统
  • 【解决办法】无法使用右键“通过VSCode打开文件夹”
  • 使用 MONAI Deploy 在 AMD GPU 上进行全身分割
  • Python酷库之旅-第三方库Pandas(193)
  • torchvision.io.write_video 报错替换
  • Jest进阶知识:React组件的单元测试
  • 微信聊天记录删了怎样才能恢复?试试这10款数据恢复软件
  • 有季节效应的非平稳序列分析
  • 简单介绍Class文件、Dex文件以及ELF文件
  • JavaScript的迭代器和生成器
  • VUE3——isRef
  • Qt使用QXlsx将Excel表格数据导入到SQLite数据库
  • HTML前端页面设计静态网站-仿百度
  • 前端笔面试查漏补缺
  • python 使用进程池并发执行 SQL 语句
  • 向量库Milvus异常挂了,重新启动
  • Docker-在Centos中部署Shell脚本获取镜像并构建容器
  • 存档库 | 《非暴力沟通》
  • 【Vue CLI 】(更新中)
  • 配电室智能巡检机器人 挂轨简易 24小时 无人值守
  • RHCE的学习(9)
  • go:embed
  • 这个操作惊呆我了!海康存储 R1竟然可以这样部署Portainer
  • 18.农产品销售系统(基于springboot和vue的Java项目)
  • 优选算法第四讲:前缀和模块
  • 对比C/C++语言,Rust语言有什么优势?