最近有读者后台留言,面试官问了他一个问题:MyBatis 如何找到要执行的 SQL 语句的?
今天展开聊聊。
要回答这个问题,我们需要搭建一个 mybatis 应用案例来深入研究 mybatis 的执行逻辑原理,这样我们就能知道 mybatis 如何找到要对应执行的 sql 语句。
1. 案例搭建
使用 idea 快速创建一个 maven 项目,pom.xml 添加如下依赖:
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.1.0</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
1.2 在 pom.xml 文件中配置 build 节点指定配置文件路径,如下:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
1.3 在 resource 目录下新建 mybatis-config.xml 文件和建 mapper 文件夹存放 sql 文件并且指定数据源连接地址和数据库账号密码、指定加载 mapper.xml 的地址如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.192.133:3306/test?useUnicode=true&useSSL=false&serverTimezone=UTC&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/MyMapper.xml" />
</mappers>
</configuration>
1.4 定义mapper接口:
public interface MyMapper {
//根据id查询用户
User getOne(@Param("id") int id);
// 查询全部用户
List<User> getAll();
}
1.5 定义对象
public class User {
private Integer id;
private String name;
private Integer age;
....
}
1.6 mapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it55.mybatis.mapper.MyMapper">
<select id="getOne" parameterType="Integer" resultType="com.it55.mybatis.domain.User">
select * from user where id=#{id}
</select>
<select id="getAll" resultType="com.it55.mybatis.domain.User">
select * from user
</select>
</mapper>
1.7 数据库表的准备,这里很简单使用了一张 User 表字段和上述对象一一对应这里就不贴出来了,创建一个 main 方法作为程序的入口 ,完整的项目目录如下图示:
2. 启动案例项目
这里先大致说下启动案例代码的步骤
1. 通过流的方式加载配置文件
2. 创建SqlSessionFactory对象
3. 根据SqlSessionFactory对象创建SqlSession对象
4. 通过SqlSession对象调用数据库操作方法
2.1 在主函数中编写以上步骤如图:
2.2 启动main方法,看运行结果:
运行没错的话这个最简单的mybatis案例,就搭建成功了,而我们接下来要做的就是一步一步来解析这个过程,代码中的指定配置文件和以流方式加载文件这两步不是代码的关键点我们直接从创建SqlSessionFactory对象说起
3.创建 sqlSessionFactory 对象过程中mybatis做了哪些事情?
我们跟进SqlSessionFactoryBuilder().build(inputStream)方法看看里面怎么实现的,
我们发现最终build方法会调用一个叫 parser.parse() 的方法,我们再跟进查看如图:
如上图所示,我们发现parser.parse()方法最终调用了parseConfiguration()方法返回值类型为Configuration,而且参数为“parser.evalNode("/configuration")”,在parseConfiguration()方法内每一个方法的参数都是对应的mybatis-config.xml文件的节点,所以我们在此可以大胆假设。
假设1:build()方法就是加载mybatis-config.xml文件中节点下信息到Configuration对象中
我们看看返回值Configuration对象中有哪些成员变量
这里为了篇幅只是截取了部分内容
现在通过断点调试来查看sqlSessionFactory对象中的configuration对象存了哪些数据,如图:
如上图所示,我们可以看到我们在配置文件中配置的数据源连接信息都被加载到了environment对象中!在此我们可以证实上文的假设1
那既然节点的信息都被加载到了configuration对象的environment成员变量中,那在节点的配置是不是也被加载到某一个对应的成员变量中?我们看configuration对象的mappedStatements成员变量:
在上图中我们可以很清晰的看到我们所写的数据库查询语句,返回值类型等信息都统统被加载到了这个mappedStatements对象中。
到此加载配置文件创建sqlSessionFactory对象先放一放我们先往下看
4. 根据SqlSessionFactory创建Sqlsession对象,做了哪些事情?
SqlSession sqlSession = sqlSessionFactory.openSession();
以上代码我们跟进查看结果如下图所示:
在上图中我们可发现openSession()方法最终调用的是openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) 这三个参数分别是,执行器类型,事物隔离级别。
事务默认是否自动提交,在debug窗口可以看到事务隔离级别参数为空的,那是不是也说明了mybatis默认的事务隔离级别是使用mysql的事务隔离级别呢?
可以看到这个方法最后返回的不单单是configuration,而且也同时返回了执行器executor类型,所以在此总结出在sqlSessionFactory对象调用openSession方法给SqlSession对象实例化了configuration和executor
5.运行sql语句的方式
5.1sqlSession运行sql语句的方式1
这种方法是直接执行sqlSession接口中操作数据库的方法,我们看默认给出了SqlSession接口中默认提供了哪些方法:
这里篇幅问题就截这两个方法吧!
我们调用SqlSession接口的selectOne(...)方法运行自己写的方法如下:第一个参数是接口方法的全限定命名,第二个参数是方法需要的参数
User one =sqlSession.selectOne("com.it55.mybatis.mapper.MyMapper.getOne", 1);
我们看看底层的selectOne()方法实现:
在上图中可以很清晰地看到在执行器执行query方法前,mybatis根据我们传入的mapper方法全限定命名去mapperStatements map中查询对应的MappedStatement,做为参数传给query方法!!到此本文的第三个问题已经得到了答案。
结论2:mybatis在创建sqlSessionFactory对象时会将mappers节点下的mapper配置加载到MappedStatements中key就是mapper接口中方法的全限定命名也就是mapper.xml文件中每个数据库操作标签的id,值就是整个MappedStatement对象这个对象中包含了数据库语句等信息,在程序执行数据库操作之前根据mapper方法的全限定命名作为key在MappedStatement对象中找到相对应的MappedStatement对象从对象中可以得到要执行的sql语句
虽然通过以上例子我们已经清楚的了解了mybatis如何找到要执行的sql语句,但是在main方法中直接使用sqlSession对象的方法时很不优雅的,在开发中也不会使用上述方式调用mapper方法,我们再继续看sqlSession运行sql语句的第二种方式
5.2 sqlSession运行sql语句的方式2
根据sqlSession.getMapper(mapper.class)方式返回mapepr接口对象,代码如下:传入的是mapper接口的calss对象
MyMapper mapper = sqlSession.getMapper(MyMapper.class);
我们看看sqlSession.getMapper(mapper.class)中做了哪些事儿?
通过上图追踪代码我们发现,sqlSession.getMapper()最终执行的是Proxy.newProxyInstance()方法,熟悉动态代理的朋友们到这应该很熟悉这个方法了。
在这我们可以假设通过sqlSession.getMapper()方式返回的mapper接口对象是mybatis为原mapper接口动态生成一个代理对象,我们可以通过返回值来证实这一结论,如下图所示:
通过上图注意mapper的值:org.apache.ibatis.binding.MapperProxy@71c8becc,可以发现返回的mapper接口并不是我们自己创建的接口而是mybatis为我们动态生成的mapper对象。
6. 通过代理对象调用getOne()方法做了哪些事儿?
在这里如果只是通过idea的ctrl+鼠标左键是无法找到真正执行的代码行的,需要在调用mapper.getOne(1)行打一个断点然后点击debug的 step into 才会找到真正执行的代码,如图:
我们点击step into进入代码如图:
在上图中我们发现调用的是invoke()方法,在动态代理中invoke()方法就是用来调用目标方法的,我们再跟进mapperMethod.execute(args);方法,如下:
在这个execute方法中,根据数据库的操作类型选择一个执行方法,在debug控制台中我们可以看到我们的数据操作类型为“SELECT“,所以最终会sqlSession.selectOne(commandName, param)方法,而这个方法接下来如何执行已经在上文的给出了明确的指引。到此通过mapper代理方式执行mapper方法的逻辑已经走完了后面的都是调用sqlsession方法里面的数据库操作方法了
最后
无论是直接调用sqlSession中方法还是通过动态代理方式最终都是由executor执行器来执行query或者update方法。同时也可以得出SqlSession接口给我们定义了操作数据库的方法,executer则是执行这些方法的最终者。
本文只是涉及讲解了configuration中mapperstatements这个对象,但mybatis还有封装参数的parameterMaps,结果集对象resultMaps等等很多重要对象都可以在configuration中找到,本文也只是mybatis框架知识的冰山一角,但我觉得通过本篇文章对mybatis的理解还是有所帮助的.