【MyBatis 源码拆解系列】执行 Mapper 接口的方法时,MyBatis 怎么知道执行的哪个 SQL?

欢迎关注公众号 【11来了】 ,持续 MyBatis 源码系列内容!

在我后台回复 「资料」 可领取编程高频电子书
在我后台回复「面试」可领取硬核面试笔记

文章导读地址:点击查看文章导读!

感谢你的关注!

MyBatis 源码系列文章:
(一)MyBatis 源码如何学习?
(二)MyBatis 运行原理 - 读取 xml 配置文件
(三)MyBatis 运行原理 - MyBatis 的核心类 SqlSessionFactory 和 SqlSession
(四)MyBatis 运行原理 - MyBatis 中的代理模式
(五)MyBatis 运行原理 - 数据库操作最终由哪些类负责?

执行 Mapper 接口的方法时,MyBatis 怎么知道执行的哪个 SQL?

功能拆解中的示例代码同样使用下边开源项目的示例 3 的代码

参考源码示例:https://github.com/yeecode/MyBatisDemo

为了阅读时可以更清晰本节的重点,会先将内容总结放在前边:

本节主要讲 MyBatis 如何解析 UserMapper.xml 中的 SQL 语句,并且在执行数据库操作时如何关联上对应的 SQL 信息。

  • 如何解析 UserMapper.xml 的 SQL 语句

MyBatis 会先解析 UserMapper.xml 文件内部的 SQL 语句,即: insert|delete|update|select 标签

解析完之后,会将标签对应的 SQL 信息包装为 MappedStatement 存储在 Configuration 的 Map 中,Map 中的 key 就是 UserMapper 的全限定类名 + 方法名

  • 如何关联对应的 SQL 信息

在真正执行 UserMapper 接口的方法时,会在 MapperProxy 拦截器中去执行真正的数据库操作,此时再根据 UserMapper 的全限定类名 + 方法名 去获取对应的 MappedStatement

接下来,正文开始

如何解析 xml 文件的 SQL 语句

之前讲了 MyBatis 的整体执行流程,使用 MyBatis 时需要先读取 mybtis-config.xml 配置文件,底层会去解析 <configuration> 标签下的内容,入口方法如下:

String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
    // 1、读取 xml 配置文件
    inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
    e.printStackTrace();
}
// 2、得到 SqlSessionFactory
SqlSessionFactory sqlSessionFactory =
        new SqlSessionFactoryBuilder().build(inputStream);

解析 xml 配置文件的地方就在 SqlSessionFactoryBuilder().build() 方法中,内部通过 XMLConfigBuilder 的 parse() 方法去解析,那么接下来直接走到内部关键方法,方法入参的就是 <configuration> 节点下的所有内容:

// MyBatis 源码 builder 包下的 XMLConfigBuilder 类
private void parseConfiguration(XNode root) {
  try { // ... 省略部分代码
    // 解析 mappers 标签的内容
    mapperElement(root.evalNode("mappers"));
  }
}

方法内部是解析 <configuration> 节点下各种标签里的内容,这里我们只关注对 <mappers> 标签的解析,该标签下的内容如下:

mybatis-config.xml

接下来走进 mapperElement() 方法:

// MyBatis 源码 builder 包下的 XMLConfigBuilder 类
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历 <mappers> 标签内部的每个 <mapper> 标签,也就是每个 xml 文件  
    for (XNode child : parent.getChildren()) { 
      // 1、获取 resource 属性值,也就是 UserMapper.xml 文件的位置
      String resource = child.getStringAttribute("resource");
        
      if (resource != null && /*省略其他的条件*/) {
        // 2、将 UserMapper.xml 读取为 InputStream  
        InputStream inputStream = Resources.getResourceAsStream(resource);  
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
        // 3、使用 XMLMapperBuilder 解析 UserMapper.xml 文件内容
        mapperParser.parse();
      // ...
      }
    }
  }
}

在这里就会读取 UserMapper.xml 文件,并且也通过专门的类 XMLMaperBuilder 去解析他内部的内容

// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    // 核心方法:解析 UserMaper.xml 中的 <mapper> 标签
    configurationElement(parser.evalNode("/mapper"));
    // ...
  }
  // ...
}

configurationElement() 方法内部,会去解析 <mapper> 标签内部的内容,也就是下图中 select 标签内部的 sql:

image-20240924112436700

进入 configurationElement() 方法内部:

// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void configurationElement(XNode context) {
  // ...
    
  // 解析增删改查标签
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

这里的 context 就是就是 UserMapper.xml 中的 <mapper> 标签内部的内容,进入 buildStatementFromContext() 方法:

// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void buildStatementFromContext(List<XNode> list) {
  // ...
  buildStatementFromContext(list, null);
}

// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  // 遍历 <mapper> 标签下的所有增删改查标签节点  
  for (XNode context : list) {
    // 1、创建解析语句的处理类 XMLStatementBuilder
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 2、解析对应语句  
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

可以看到最终 <select> 标签的解析工作还是交给了对应的处理器 XMLStatementBuilder 来完成,并且解析式所需要的参数,在创建时直接就传入了,在该类的 parseStatementNode() 方法中,真正去解析 <select> 标签的内容,并且将解析到的内容存储下来:

// MyBatis 源码 builder 包下的 XMLStatementBuilder 类
public void parseStatementNode() {
  // ...
  // 这里 id 是执行 UserMapper 中的方法名:queryUserBySchoolName
  String id = context.getStringAttribute("id");
  // nodeName 就是 select 标签的名字,即:select
  String nodeName = context.getNode().getNodeName();
    
  // 判断语句类型,查询时 sqlCommandType = SELECT
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  
  // 解析返回值类型,这里为:com.github.yeecode.mybatisdemo.User
  String resultType = context.getStringAttribute("resultType");

    // 下边的入参是从 <select> 标签内部解析出来的数据
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

上边经过一系列解析操作,这里省略了很多,最终解析出来的参数都会通过 MapperBuilderAssistant 助手来构造 MappedStatement 并进行存储,如下:

// MyBatis 源码 builder 包下的 MapperBuilderAssistant 类
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, /*参数过多省略*/) {
  // 将 id 拼接上 namespace,即:com.github.yeecode.mybatisdemo.UserMapper + queryUserBySchoolName
  id = applyCurrentNamespace(id, false);
  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource)
      // ... builde 的属性太多省略
      ;
  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  
  // 创建 MappedStatement
  MappedStatement statement = statementBuilder.build();
  // 将 MappedStatement 放入到 Configuration 的 Map 中
  // 在 Map 中存储的 Key 就是 namespace + id
  configuration.addMappedStatement(statement);
  return statement;
}

在这里通过 MappedStatement 里边的 Builder 来构建 MappedStatement 对象,最终 MappedStatement 对象就被放入到 Configuration 中去,UserMapper.xml 中的 <select> 标签解析到 Configuration 的内容如下图:

image-20240924230242063

如何关联对应的 SQL 信息

在执行 SQL 的时候,如何找到该 SQL 对应的 MappedStatement 呢?

首先,在之前的文章 MyBatis 运行原理中已经说了,当执行 UserMapper 方法时,会进入到 MapperProxy 拦截器中,最终会走到 DefaultSqlSession 对应的查询方法: getList()

// MyBatis 源码 session 包下的 DefaultSqlSession 类
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    // 根据 statement 获取对应的 MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  }
  // ...
}

这里根据 statement 作为 key 去 Configuration 中获取对应的 MappedStatement

statement 的值就是 UserMapper 的全限定类名 + 接口的方法名com.github.yeecode.mybatisdemo.UserMaper.queryUserBySchoolName

获取到 MappedStatement 之后,就交给执行器 Executor 去执行对应的 SQL 语句了,MappedStatement 的部分内容如下:

  • sqlSource:存储了 <select> 标签内部的动态 SQL
  • id:唯一表示,UserMapper 的接口名 + 方法名
  • resource:对应的 xml 文件

image-20240924221020051

总结

最后再总结一下,学完本节可以收获什么?

  • MyBatis 内部是如何对 SQL 语句的信息进行解析和存储?
  • MyBatis 针对这个功能设计了哪些类?每个类的职责如何划分?

MyBatis 是一个 ORM 框架,作为后端应用和数据库之间的 桥梁 ,目的是帮助研发人员管理和执行 SQL 语句,将 SQL 语句定义在 xml 文件之后,MyBatis 肯定需要存储起来,并且在执行 UserMapper 接口对应的方法时,可以找到对应的 SQL 语句以及对应的一些信息,包括参数类型、返回值类型等等

MyBatis 就是通过 MappedStatement 这个对象来存储的,通过唯一标识 UserMapper 接口全限定类名 + 方法名 来确定唯一的 MappedStatement

创建 MappedStatement 的地方是在解析 xml 文件时完成的,主要涉及三个类:

  • XMLConfigBuilder: 解析 mybatis-config.xml 文件
  • XMLMapperBuilder: 解析 UserMapper.xml 文件中 <mapper> 标签里的内容
  • XMLStatementBuilder: 解析 UserMapper.xml 文件中 <mapper> 标签内增删改查标签的内容,如 <select> | <update> ...

可以看到在解析的过程中,通过一层一层的职责拆分,不断将每个类的职责进行细化,避免一个类负责多种类型的任务

解析之后,将 MappedStatement 放入到全局配置类 Configuration 的 Map 中,当执行 Mapper 接口中的方法时,通过 【Mapper 接口的全限定类名 + 方法名】作为唯一标识来获取对应的 MappedStatement,就可以获取该接口对应 SQL 的一些信息了

整体流程如下:

image-20240924230316463
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

11来了

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

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

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

打赏作者

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

抵扣说明:

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

余额充值