硬核干货,万字长文带你从根上了解 MyBatis 的运行过程
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。现在让我们来探寻一下 Mybatis 的奥秘,源码比较多谨慎食用。
搭建一个简单的 MyBatis 项目
我们先来搭建一个简单的 MyBatis
项目,以此来展开对 MyBatis
的分析。
引入 maven 文件
<dependencies>
<!-- 引入 mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- 引入 mysql 连接器 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- 引入 lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
</dependencies>
创建配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="org.example.mybatis.mapper"/>
</mappers>
</configuration>
创建表
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='test';
创建对应的实体以及 Mapper
@Data
public class Test {
private Long id;
private String name;
}
public interface TestMapper {
@Select("select * from test where id = #{id}")
Test findById(Long id);
}
执行查询方法
public class Main {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
TestMapper mapper = session.getMapper(TestMapper.class);
Test test = mapper.findById(1L);
System.out.println(test);
}
}
}
通过上面的例子可以看到使用 MyBatis
与数据库交互的步骤是:
- 从
XML
文件中构建SqlSessionFactory
。 - 从
SqlSessionFactory
中获取SqlSession
。 - 使用
SqlSession
获取对应的Mapper
对象。 - 通过
Mapper
对象执行相应的方法。
MyBatis 执行流程
本节会简述一下 Mybatis
的核心对象以及 MyBatis
的执行流程。
核心对象简介
MapperProxy
: 用于生成Mapper
接口的代理类。SqlSession
:作为MyBatis
工作的主要顶层API
,表示和数据库交互的会话,完成必要数据库增删改查功能。Executor
:MyBatis
执行器,是MyBatis
调度的核心,负责SQL
语句的生成和查询缓存的维护。StatementHandler
:封装了JDBC Statement
操作,负责对JDBC Statement
的操作。ParameterHandler
:负责对用户传递的参数转换成JDBC Statement
所需要的参数。ResultSetHandler
:负责将JDBC
返回的ResultSet
结果集对象转换成List
类型的集合。TypeHandler
:负责java
数据类型和JDBC
数据类型之间的映射和转换。
执行流程
MyBatis
的执行流程图如下所示:
我们先简述一下 MyBatis
的执行流程,后面将会详细介绍各个细节。
Mapper
调用增删改查操作时,实际上是调用了MapperProxy.invoke()
方法。MapperProxy.invoke()
调用了SqlSession
提供的数据库交互的API
。SqlSession
与把与数据库交互的工作交给Executor
去执行。Executor
负责SQL
语句的生成和查询缓存的维护,将与数据库连接的工作交给StatementHandler
。StatementHandler
封装了JDBC Statement
操作,他通过ParameterHandler
将 用户传递的参数转换成JDBC Statement
所需要的参数,然后使用JDBC
真正的操作数据库,然后用ResultSetHandler
将JDBC
返回的ResultSet
结果集对象转换成List
类型的集合。
MyBatis 核心对象介绍
MapperProxy
通过 session.getMapper(TestMapper.class)
获取的类反编译的结果为:
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* org.example.mybatis.entity.Test
* org.example.mybatis.mapper.TestMapper
*/
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import org.example.mybatis.entity.Test;
import org.example.mybatis.mapper.TestMapper;
public final class $Proxy3
extends Proxy
implements TestMapper {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy3(InvocationHandler invocationHandler) {
super(invocationHandler);
}
// ...省略部分无关代码
public final Test findById(Long l) {
try {
return (Test)this.h.invoke(this, m3, new Object[]{l});
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("org.example.mybatis.mapper.TestMapper").getMethod("findById", Class.forName("java.lang.Long"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
}
catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
}
通过上面的代码我们看到我们在调用 findById
时真正调用的则是 return (Test)this.h.invoke(this, m3, new Object[]{l});
。大家能会疑惑 this.h
是什么,它其实是 Proxy
类的一个成员变量 protected InvocationHandler h;
。下面让我们看一下这个 h
它到底是哪个类,这个代理类最终是通过 MapperProxyFactory.newInstance()
生成的。通过下面的代码可以看出 h
其实就是 MapperProxy
。
public class MapperProxyFactory<T> {
// 省略...
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 将 MapperProxy 作为 InvocationHandler 生成代理类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
我们来看一下 MapperProxy
的 invoke()
方法,它是最后调用了 MapperMethod.execute()
。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 通过 MapperMethod 来执行
return mapperMethod.execute(sqlSession, args);
}
而 MapperMethod.execute()
最终是通过 SqlSession
来执行真正的逻辑。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
// sqlSession 调用插入方法
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
// 省略部分代码
return result;
}
到这里我们就捋清了怎样获取 Mapper
以及到最后使用 SqlSession
与数据库交互的流程,下面我们将看一下 SqlSession 是怎样和数据库交互的。
SqlSession
SqlSession
在 MyBatis
中是非常强大的一个类。它包含了所有执行语句、提交或回滚事务以及获取映射器实例的方法。
DefaultSqlSession
DefaultSqlSession
是 SqlSession
的默认实现类。DefaultSqlSession
中使用到了策略模式,DefaultSqlSession
扮演了 Context
的角色,而将所有数据库相关的操作全部封装到 Executor
接口实现中,并通过 executor
字段选择不同的 Executor
实现。
DefaultSqlSessionFactory
DefaultSqlSessionFactory
是一个具体工厂类,实现了 SqlSessionFactory
接口。DefaultSqlSessionFactory
主要提供了两种创建 DefaultSqlSession
的方式,一种方式通过数据源获取数据库连接,并创建 Executor
对象以及 DefaultSqlSession
,该方式的具体实现如下所示:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获取配置文件中的Environment对象
final Environment environment = configuration.getEnvironment();
// 获取TransactionFactory对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建Transaction对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据配置创建Executor对象
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 关闭Transaction
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
另一种方式是用户提供数据库连接对象 DefaultSqlSessionFactory
使用数据库连接对象创建 Executor
对象以及 DefaultSqlSession
对象,具体实现如下:
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
// 获取当前连接的事务是否为自动提交方式
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
// 当前数据库驱动提供的连接不支持事务,则可能会抛 异常
autoCommit = true;
}
// 获取配置文件中的Environment对象
final Environment environment = configuration.getEnvironment();
// 获取TransactionFactory对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建Transaction对象
final Transaction tx = transactionFactory.newTransaction(connection);
// 根据配置创建Executor对象
final Executor executor = configuration.newExecutor(tx, execType);
// 创建DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
DefaultSqlSessionFactory
提供的所有 openSession()
方法重载都是基于上述两种方式创建 DefaultSqlSession
对象的,这里不再赘述。
SqlSessionManager
SqlSessionManager
同时实现 SqlSession
接口和 SqlSessionFactory
接口,也就同时提供了 SqlSessionFactory
创建 SqlSession
以及 SqlSession
操纵数据库的功能。
SqlSessionManager
各个字段的含义如下:
// 底层封装的SqlSessionFactory对象
private final SqlSessionFactory sqlSessionFactory;
// localSqlSession中记录的SqlSession对象的代理对象
private final SqlSession sqlSessionProxy;
// ThreadLocal变量,记录一个与当前线程绑定的SqlSession对象
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
SqlSessionManager
和 DefaultSqlSessionFactory
的主要不同点是 SqlSessionManager
提供了两种模式:一种模式与 DefaultSqlSessionFactory
的行为相同,同一线程每次通过 SqlSessionManager
对象访问数据库时,都会创建新的 DefaultSession
对象完成数据库操作;第二种模式是 SqlSessionManager
通过 localSqlSession
这个 ThreadLocal
,记录与当前线程绑定的 SqlSession
对象,供当前线程循环使用,从而避免在同一线程多次创建 SqlSession
对象带来的性能损失。
首先看 SqlSessionManager
的构造方法,其构造方法都是私有的,如果要创建 SqlSessionManager
对象,需要调用其 newInstance()
方法。
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
// 使用动态代理的方式生成SqlSession的代理对象
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SqlSessionInterceptor());
}
public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionManager(sqlSessionFactory);
}
SqlSessionManager.openSession()
方法以及其重载是直接通过调用其中底层封装的 SqlSessionFactory.openSession
方法来创建 SqlSession
对象的。
SqlSessionManager
中实现的 SqlSession
接口方法,例如 select*()、update()
等,都是直接调用 sqlSessionProxy
字段记录的 SqlSession
代理对象的相应方法实现的。在创建该代理对象时使用的 InvocationHandler
对象是 SqlSessionlnterceptor
对象,它是定义在 SqlSessionManager
中的内部类,invoke()
方法实现如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取当前线程绑定的SqlSession对象
final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
if (sqlSession != null) {
try {
// 调用真正的SqlSession对象,完成数据库的相关操作
return method.invoke(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} else {
try (SqlSession autoSqlSession = openSession()) {
try {
// 通过新建SqlSession对象完成数据库操作
final Object result = method.invoke(autoSqlSession, args);
// 提交事务
autoSqlSession.commit();
return result;
} catch (Throwable t) {
// 回滚事务
autoSqlSession.rollback();
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
通过对 SqlSessionInterceptor
的分析可知,第一种模式中新建的 SqlSession
在使用完成后会关闭。在第二种模式中与当前线程绑定的 SqlSession
对象需要先通过 SqlSessionManager.startManagedSession()
方法进行设置,具体实现如下:
public void startManagedSession() {
this.localSqlSession.set(openSession());
}
当需要提交/回滚事务或是关闭 localSqlSession
中记录的 SqlSession
对象时,需要通过 SqlSessionManager.commit()、rollback()
以及 close()
方法完成,其中会先检测当前线程是否绑定 SqlSession
对象,如果未绑定则抛出异常 ,如果绑定了则调用该 SqlSession
对象的相应方法。
Executor
Executor
是 MyBatis
的核心接口之一,其中定义了数据库操作的基本方法。在实际应用中经常涉及的 SqlSession
接口的功能,都是基于 Executor
接口实现的。Executor
接口中定义的方法如下:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// 执行insert、update、delete等操作
int update(MappedStatement ms, Object parameter) throws SQLException;
// 执行 select类型的SQL语句,返回位分为结果对象列表或游标对象
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 批量执行SQL语句
List<BatchResult> flushStatements() throws SQLException;
// 提交事务
void commit(boolean required) throws SQLException;
// 回滚事务
void rollback(boolean required) throws SQLException;
// 创建缓存中用的CacheKey对象
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 根据CacheKey对象查找缓存
boolean isCached(MappedStatement ms, CacheKey key);
// 清空一级缓存
void clearLocalCache();
// 延迟加载一级缓存中的数据
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
// 获取事务对象
Transaction getTransaction();
// 关闭Executor对象
void close(boolean forceRollback);
// 检测Executor是否已关闭
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
BaseExecutor
BaseExecutor
是一个实现了 Executor
接口的抽象类,它实现了 Executor
接口的大部分方法,其中就使用了模板方法模式。BaseExecutor
主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor
的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别 doUpdate()
方法、doQuery()
方法、doQueryCursor()
方法、doFlushStatement()
方法,其余的功能在 BaseExecutor
实现。
BaseExecutor
各个字段的含义如下:
// Transaction对象,实现事务的提交、回滚和关闭操作
protected Transaction transaction;
// 封装的Executor对象
protected Executor wrapper;
// 延迟加载队列
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 一级缓存,用于缓存该Executor对象查询结果集映射得到的结果对象
protected PerpetualCache localCache;
// 一级缓存,用于缓存输出类型的参数
protected PerpetualCache localOutputParameterCache;
// 用来记录嵌套查询的层数
protected int queryStack;
一级缓存
执行 select
语句查询数据库是最常用的功能,BaseExecutor.query()
方法会首先创建 CacheKey
对象,并根据该 CacheKey
对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果缓存未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。query()
方法的具体实现如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建CacheKey对象
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 调用query()的一个重载
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
这里关注 BaseExecutor.createCacheKey()
方法创建的 CacheKey
对象由哪几部分构成,createCacheKey()
方法具体实现如下:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
// 检测Executor是否关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// 添加MappedStatement的id
cacheKey.update(ms.getId());
// 添加offset
cacheKey.update(rowBounds.getOffset());
// 添加limit
cacheKey.update(rowBounds.getLimit());
// 添加SQL语句
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
// 获取实参并添加到CacheKey
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
// 添加Environment的id
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
由此可知该 CacheKey
对象由 MappedStatement
的 id
、对应的 offse、limit、SQL
语句(包含 ?
占位符)、用户传递的实参以及 Environment.id
这五部分构成。
继续来看上述代码中调用的 query()
方法的另一重载的具体实现,该重载会根据前面创建的 CacheKey
对象查询一级缓存,如果缓存命中则将缓存中记录的结果对象返回,如果缓存未命中,则调用 doQuery()
方法完成数据库的查询操作并得到结果对象,之后将结果对象记录到一级缓存中。具体实现如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 检测Executor是否关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 非嵌套查询,并且<select>节点配置的flushCache属性为true时,才会清空一级缓存
clearLocalCache();
}
List<E> list;
try {
// 增加查询层数
queryStack++;
// 查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 针对存储过程调用的处理,其功能是:在一级缓存命中时,获取缓存中保存的输出类型参数,并设置到用户传入的实参对象中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 调用doQuery()方法完成数据库查询,并得到映射后的结果对象,
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// 当前查询完成,层数减少
queryStack--;
}
// 延迟加载
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
上面介绍了 BaseExecutor
中缓存的第一种功能,也就是缓存查询得到的结果对象。除此之外,一级缓存还有第二个功能:前面在分析嵌套查询时,如果一级缓存中缓存了嵌套查询的结果对象,则可以从一级缓存中直接加载该结果对象;如果一级缓存中记录的嵌套查询的结果对象并未完全加载,则可以通过 DeferredLoad
实现类似延迟加载的功能。
Executor
中与上述功能直接相关的方法有两个,一个是 isCached()
方法负责检测是否缓存指定查询的结果对象,具体实现如下:
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
// 检测缓存中是否缓存了对应的对象
return localCache.getObject(key) != null;
}
另一个是 deferLoad()
方法,它负责创建 DeferredLoad
对象并将其添加到 deferredLoads
集合中,具体实现如下:
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return doQueryCursor(ms, parameter, rowBounds, boundSql);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建DeferredLoad对象
DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
if (deferredLoad.canLoad()) {
// 一级缓存中已经记录了指定查询的结果对象 直接从缓存中加载对象,并设置到外层对象中
deferredLoad.load();
} else {
// 将DeferredLoad对象添加到deferredLoads队列中,待整个外层查询结束后,再加载该结果对象
deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
}
}
DeferredLoad
是定义在 BaseExecutor
中的内部类,它负责从 localCache
缓存中延迟加载结果对象,其字段的含义如下:
// 外层对象对应的MetaObject对象
private final MetaObject resultObject;
// 延迟加载的局生名称
private final String property;
// 延迟加载的属性的类型
private final Class<?> targetType;
// 延迟加载的结果对象在一级缓存中相应的CacheKey对象
private final CacheKey key;
// 一级缓存
private final PerpetualCache localCache;
private final ObjectFactory objectFactory;
// 负责结采对象的类型转换
private final ResultExtractor resultExtractor;
DeferredLoad.canLoad()
方法负责检测缓存项是否已经完全加载到了缓存中。首先要说明完全加载的含义BaseExecutor.queryFromDatabase()
方法中,开始查询调用 doQuery()
方法查询数据库之前,会先在 localCache
中添加占位符,待查询完成之后才将真正的结果对象放到 localCache
中缓存,此时该缓存项才算完全加载。BaseExecutor.queryFromDatabase()
方法具体实现如下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在缓存中添加占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 完成数据库查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除占位符
localCache.removeObject(key);
}
// 将真正的结果对象放入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
// 缓存输出类型的参数
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
DeferredLoad.canLoad()
方法的具体实现如下:
public boolean canLoad() {
// 检测缓存是否存在指定的结果对象和是否为占位符
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
DeferredLoad.load()
方法负责从缓存中加载结果对象,并设置到外层对象的相应属性中,具体实现如下:
public void load() {
@SuppressWarnings("unchecked")
// we suppose we get back a List
// 从缓存中查询结果对象
List<Object> list = (List<Object>) localCache.getObject(key);
// 将结果对象转换成指定类型
Object value = resultExtractor.extractObjectFromList(list, targetType);
// 设置到外层对象的对应属性
resultObject.setValue(property, value);
}
介绍完 DeferredLoad
对象之后,来看触发 DeferredLoad
缓存中加载结果对象的相关代码,这段代码在 BaseExecutor.query()
方法中,具体实现如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// ...
// 延迟加载
if (queryStack == 0) {
// 在最外层的查询结束时,所有嵌套查询也已经完成,相关缓存项也已经完全加载,所以在这里可以触发DeferredLoad加载一级缓存中记录的嵌套查询的结果对象
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 加载完成后清空deferredLoads
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// 根据localCacheScope配置决定是否清空一级缓存
clearLocalCache();
}
}
return list;
}
BaseExecutor.queryCursor()
方法的主要功能也是查询数据库,这一点与 query()
方法类似,但它不会直接将结果集映射为结果对象,而是将结果集封装成 Cursor
对象并返回,待用户遍历 Cursor
时才真正完成结果集的映射操作。另外,queryCursor()
方法是直接调用 doQueryCursor()
这个基本方法实现的,并不会像 query()
方法那样使用查询一级缓存。
BaseExecutor.update()
方法负责执行 insert、update、delete
三类 SQL
语句,它是调用 doUpdate()
模板方法实现的。在调用 doUpdate()
方法之前会清空缓存,因为执行 SQL
语句之后,数据库中的数据已经更新,一级缓存的内容与数据库中的数据可能已经不一致了,所以需要调用 clearLocalCache()
方法清空一级缓存中的脏数据。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// 判断当前Executor是否已经关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清楚一级缓存
clearLocalCache();
// 调用doUpdate()方法
return doUpdate(ms, parameter);
}
事务相关操作
在 BatchExecutor
实现中,可以缓存多条 SQL
语句,等待合适的时机将缓存的多条 SQL
语句,并发送到数据库执行。 Executor.flushStatements()
方法主要是针对批处理多条 SQL
语句的,它会调用 doFlushStatements()
这个基本方法处理 Executor
缓存的多条 SQL
语句。在BaseExecutor.commit()、rollback()
等方法中都会首先调用 flushStatements()
方法,然后再执行相关事务操作。
BaseExecutor.flushStatements()
方法的具体实现如下:
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
// 判断当前Executor是否已经关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 调用doFlushStatements()这个基本方法,其参数isRollBack表示是否执行Executor中缓存的SQL语句,false表示执行,true表示不执行
return doFlushStatements(isRollBack);
}
BaseExecutor.commit()
方法首先会清空一级缓存、调用 flushStatements()
方法,最后才根据参数决定是否真正提交事务。commit()
方法的实现如下:
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空一级缓存
clearLocalCache();
// 执行缓存的SQL语句,其中调用了flushStatements(false)方法
flushStatements();
// 根据required参数决定是否提交事务
if (required) {
transaction.commit();
}
}
BaseExecutor.rollback()
方法的实现与 commit()
实现类似,同样会根据参数决定是否真正回滚事务 ,区别是其中调用的是 flushStatements()
方法的 isRollBack
参数为 true
这就会导致 Executor
中缓存的 SQL
语句全部被忽略,即不会被发送到数据库执行。
BaseExecutor.close()
方法首先会调用 rollback()
方法忽略缓存的SQL语句,之后根据参数决定是否关闭底层的数据库连接。
SimpleExecutor
SimpleExecutor
继承了 BaseExecutor
抽象类,它是最简单的 Executor
接口实现。正如前面所说,Executor
使用了模板方法模式,一级缓存等固定不变的操作都封装到了BaseExecutor
。SimpleExecutor
中就不必再关心一级缓存等操作,只需要专注实现四个基本方法的实现即可。
首先来看 SimpleExecutor.doQuery()
方法的具体实现:
@Override
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对象 ,实际返回的是RoutingStatementHandler对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 完成Statement的创建和初始化
stmt = prepareStatement(handler, ms.getStatementLog());
// 调用StatementHandler .query()方法,执行SQL语句,并通过ResultSetHandler,完成结果集的映射
return handler.query(stmt, resultHandler);
} finally {
// 关闭Statement对象
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 创建Statements对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 处理占位符
handler.parameterize(stmt);
return stmt;
}
SimpleExecutor.doQueryCursor()
方法、doUpdate()
方法与 doQuery()
方法实现类似,这里不再分析。 SimpleExecutor
不提供批量处理 SQL
语句的功能,所以其 doFlushStatements()
方法直接返回空集合,不做其他任何操作。
ReuseExecutor
在传统的 JDBC
编程中,重用 Statement
对象是常用的一种优化手段,该优化手段可以减少 SQL
预编译的开销以及创建和销毁 Statement
对象的开销,从而提高性能。
ReuseExecutor
提供了 Statement
重用的功能,ReuseExecutor
中通过 statementMap
字段缓存使用过的 Statement
对象,key
是 SQL
语句,value
是 SQL
对应的 Statement
对象。ReuseExecutor.doQuery()、doQueryCursor()、doUpdate()
方法的实现与S impleExecutor
对应方法的实现一样,区别在于其中调用的 preparestatement()
方法,SimpleExecutor
每次都会通过 JDBC Connection
创建新的 Statement
对象,而 ReuseExecutor
会先尝试重用 StaternentMap
缓存的Statement
对象。
ReuseExecutor.prepareStatement()
方法的具体实现如下:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获取BoundSql对象
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 判断该SQL是否缓存过Statement
if (hasStatementFor(sql)) {
// 获取缓存的Statement对象
stmt = getStatement(sql);
// 修改超时时间
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
// 创建新的Statement对象,并缓存到statementMap集合中
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
当事务提交或回滚、连接关闭时,都需要关闭这些缓存的 Statement
对象 。前面介绍了 BaseExecutor.commit()、 rollback()
和 close()
方法时提到,其中都会调用 doFlushStatements()
方法,所以在该方法实现关闭 Statement
对象的逻辑非常合适,具体实现如下:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {
// 遍历statementMap集合并关闭其中的Statement对象
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
// 清空statementMap
statementMap.clear();
return Collections.emptyList();
}
这里需要注意一下 ReuseExecutor.queryCursor()
方法的使用,熟悉 JDBC
编程的读者知道,每个 Statement
对象只能对应一个结果集,当多次调用 queryCursor()
方法执行同 SQL
语句时,会复用 Statement
对象,只有最后一个 ResultSet
是可用的。而 queryCursor()
方法返回的 Cursor
对象,在用户迭代 Cursor
对象时,才会真正遍历结果集对象并进行映射操作,这就可能导致使用前面 Cursor
对象中封装的结果集关闭。
BatchExecutor
应用系统在执行一条 SQL
语句时,会将 SQL
语句以及相关参数通过网络发送到数据库系统。对于频繁操作数据库的应用系统来说,如果执行 SQL
语句就向数据库发送一次请求,很多时间会浪费在网络通信上。使用批量处理的优化方式可以在客户端缓存多条 SQL
语句,并在合适的时机将多条 SQL
语句打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。
不过有一点需要注意,在批量执行多条 SQL
语句时,每次向数据库发送的 SQL
语句条数是有上限的,如果超过这个上限,数据库会拒绝执行这些 SQL
语句井抛出异常 所以批量发送 SQL
语句的时机很重要。
BatchExecutor
实现了批处理多条 SQL
语句的功能,其中核心字段的含义如下:
// 缓存多个Statement对象其中每个Statement对象中都缓存了多条SQL语句
private final List<Statement> statementList = new ArrayList<>();
// 记录批处理的结果,BatchResult中通过updateCounts字段记录每个Statement执行批处理的结果
private final List<BatchResult> batchResultList = new ArrayList<>();
// 记录当前执行的SQL语句
private String currentSql;
// 记录当前执行的MappedStatement对象
private MappedStatement currentStatement;
JDBC
中的批处理只支持 insert、update、delete
等类型的 SQL
语句,不支持 select
类型的 SQL
语句,所以下面要分析的是 BatchExecutor.doUpdate
方法。
BatchExecutor.doUpdate()
方法在添加一条 SQL
语句时,首先会将 currentSql
字段记录的 SQL
语句以及 currentStatement
字段记录的 MappedStatement
对象与当前添加的 SQL
以及 MappedStatement
对象进行比较,如果相同则加到同一个 Statement
对象等待执行,如果不同则创建新的 Statement
对象并将其缓存到 statementList
集合中等待执行。doUpdate()
方法具体实现如下:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
// 获取配置对象
final Configuration configuration = ms.getConfiguration();
// 创建StatementHandler对象
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
// 获取SQL语句
final String sql = boundSql.getSql();
final Statement stmt;
// 如果当前执行的SQL模式与上次执行的SQL模式相同且对应的MappedStatement对象相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 获取statementList中的最后一个
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
// 绑定实参
handler.parameterize(stmt);
// 查找对应的BatchResult对象,并记录用户传入的实参
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
// 创建新的Statement对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 绑定实参
handler.parameterize(stmt);
// 更新currentSql、currentStatement
currentSql = sql;
currentStatement = ms;
// 添加刚创建的Statement对象
statementList.add(stmt);
// 添加新的BatchResult对象
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 底层通过调用Statement.addBatch()方法添SQL语句
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
JDBC
批处理功能中 Statement
可以添加不同模式的 SQL
,但是每添加一个新模式的 SQL
语句都会触发一次编译操作 PreparedStatement
中只能添加同一模式的 SQL
语句,只会触发一次编译操作,但是可以通过绑定多组不同的实参实现批处理。通过上面对 doUpdate()
方法的分析可知,BatchExecutor
会将连续添加的、相同模式的 SQL
语句添加到同一个 Statement/PreparedStatement
对象中,这样可以有效地减少编译操作的次数。
在添加完待执行的SQL语句之后来看一下 BatchExecutor.doFlushStatemnts()
方法是如何批量处理这些 SQL
语句的:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
// 用于储存批量处理的结果
List<BatchResult> results = new ArrayList<>();
// 如果明确指定了要回滚事务,则直接返回空集合,忽略statementList集合中记录的SQL语句
if (isRollback) {
return Collections.emptyList();
}
// 遍历statementList集合
for (int i = 0, n = statementList.size(); i < n; i++) {
// 获取Statement对象
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
// /获取对应的BatchResult对象
BatchResult batchResult = batchResultList.get(i);
try {
// 调用Statement.executeBatch()方法批量执行其中记录的SQL语句,并使用返回的int数组
// 更新BatchResult.updateCounts字段,其中每一个元素都表示一条SQL语句影响的记录条数
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
// 获取配置的KeyGenerator对象
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
// 获取数据库生成的主键,并设置到parameterObjects中
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
// 对于其他类型的keyGenerator,会调用其processAfter()方法
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
// 关闭Statement对象
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
// 添加BatchResult到results集合
results.add(batchResult);
}
return results;
} finally {
// 关闭Statement对象,并清空currentSql字段、清空statementList集合、清空batchResultList集合
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
BatchExecutor.doQuery
和 doQueryCursor()
方法的实现与前面介绍的 SimpleExecutor
类似,主要区别就是 BatchExecutor
中的这两个方法在最开始都会先调用 flushStatement()
方法,执行缓存 SQL
语句,这样才能从数据库中查询到最新的数据。
CachingExecutor
CachingExecutor
是 Executor
接口的装饰器,它为 Executor
对象增加了二级缓存的相关的功能,先来简单介绍 MyBatis
中的二级缓存及其依赖的相关组件。
二级缓存简介
MyBatis
中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个:
(1)首先是 mybatis-config.xml
配置文件中的 cacheEnabled
配置,它是二级缓存的总开关。只有当该配置设置为true
时,后面两项的配置才会有效果,cacheEnabled
的默认值为 true
。具体配置如下:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
(2)在前面介绍映射配置文件的解析流程时提到,映射配置文件中可以配置 <cache>
节点或 <cache-ref>
节点。如果映射配置文件中配置了这两者中的任何一个节点,则表示开启了二级缓存功能。如果配置了 <cache>
节点,在解析时会为该映射配置文件指定的命名空间创建相应的 Cache
对象作为其二级缓存,默认是 PerpetualCache
对象,用户可以通过 <cache>
节点的 type
属性指定自定义 Cache
对象。如果配置了<cache-ref>
节点,在解析时则不会为当前映射配置文件指定的命名 空间创建独立的 Cache
对象,而是认为它与 <cache-ref>
节点的 namespace
属性指定的命名空间共享同一个 Cache
对象。通过 <cache>
节点和<cache-ref>
节点的配置,用户可以在命名空间的粒度上管理二级缓存的开启和关闭。
(3)最后一个配置项是 <select>
节点中的 useCache
属性,该属性表示查询操作产生的结果对象是否要保存到二级缓存中。useCache
属性的默认值是 true
。
TransactionalCache&TransactionalCacheManager
TransactionalCache
和 TransactionalCacheManager
是 CachingExecutor
依赖的两个组件。其中,TransactionalCache
继承了Cache
接口,主要用于保存在某个SqlSession
的某个事务中需要向某个二
级缓存中添加的缓存数据。TransactionalCache中核心字段的含义如下:
// 底层封装的二级缓存所对应的Cache对象
private final Cache delegate;
// 当该字段为true时,则表示当前TransactionalCache不可查询,且提交事务时会将底层Cache清空
private boolean clearOnCommit;
// 暂时记录添加到TransactionalCache中的数据。在事务提交时,会将其中的数据添加到二级缓存中
private final Map<Object, Object> entriesToAddOnCommit;
// 记录缓存未命中的CacheKey对象
private final Set<Object> entriesMissedInCache;
TransactionalCache.putObject()
方法并没有直接将结果对象记录到其封装二级缓存中,而是暂时保存在 entriesToAddOnCommit
集合中,在事务提交时才会将这些结果对象从 entriesToAddOnCommit
集合添加到二级缓存中。putObject()
方法的具体实现如下:
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
再来看 TransactionalCache.getObject()
方法,它首先会查询底层的二级缓存,并将未命中的 key
添加到 entriesMissedInCache
中,之后会根据 clearOnCommit
字段的值决定具体的返回值,具体实现如下:
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
// 将未命中的key添加到entriesMissedInCache中
entriesMissedInCache.add(key);
}
// issue #146
// 根据clearOnCommit字段的值决定具体的返回值
if (clearOnCommit) {
return null;
} else {
return object;
}
}
TransactionalCache.clear()
方法会清空 entriesToAddOnCommit
集合,并设置 clearOnCommi
为 true
。
TransactionalCache.commit()
方法会根据 clearOnCommit
字段的值决定是否清空二级缓存,然后调用 flushPendingEntries()
方法将 entriesToAddOnCommit
集合中记录的结果对象保存到二级缓存中,具体实现如下:
public void commit() {
// 在事务提交前 清空二级缓存
if (clearOnCommit) {
delegate.clear();
}
// 将entriesToAddOnCommit集合中的数据保存到二级缓存
flushPendingEntries();
// 重置clearOnCommit为false ,并清空entriesToAddOnCommit和entriesMissedInCache 集合
reset();
}
private void flushPendingEntries() {
// 遍历entriesToAddOnCommit集合,将其中记录的缓存项添加到二级缓存中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
TransactionalCache.rollback()
方法会将 entriesMissedlnCache
集合中记录的缓存项从二级缓存中删除,并清空 entriesToAddOnCommit
集合和 entriesMissedlnCache
集合,具体实现如下:
public void rollback() {
// 将entriesMissedInCache集合中记录的缓存项从二级缓存中删除
unlockMissedEntries();
// 遍历entriesMissedInCache集合,将entriesToAddOnCommit集合中不包含的缓存项添加到二级缓存中
reset();
}
TransactionalCacheManager
用于管理 CachingExecutor
使用的二级缓存对象,其中只定义了transactionalCaches
字段,它的 key
是对应的 CachingExecutor
使用的二级缓存对象,value
是相应 TransactionaCach
对象,在该 TransactionalCache
中封装了对应二级缓存对象,也就是这里的 key
。
TransactionalCacheManager
的实现比较简单,下面简单介绍各个方法的功能和实现。
clear()方法、 putObject方法、 getObject()方法: 调用指定二级缓存对应的 TransactionalCache
对象的对应方法,如果 transactionalCaches
集合中没有对应的 TransactionalCache
对象,则通过 getTransactionalCache()
方法创建。
private TransactionalCache getTransactionalCache(Cache cache) {
// 如果transactionalCaches集合中没有对应的TransactionalCache对象,则新建
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
commit()方法、 rollback()方法: 遍历 transactionalCaches
集合,井调用其中各个 TransactionalCache
对象的相应方法。
CachingExecutor的实现
CachingExecutor.query()
方法执行查询操作的步骤如下:
(1)获取 BoundSql
对象,创建查询语句对应的 CacheKey
对象。
(2)检测是否开启了二级缓存,如果没有开启二级缓存,则直接调用底层 Executor
对象的 query()
方法查询数据库。如果开启了二级缓存,则继续后面的步骤。
(3)检测查询操作是否包含输出类型的参数,如果是这种情况,则报错
(4)调用 TransactionalCacheManager.getObject()
方法查询二级缓存,如果二级缓存中查找到相应的结果对象,则直接将该结果对象返回。
(5)如果二级缓存没有相应的结果对象,则调用底层 Executor
对象的 query()
方法。正如前面介绍的 ,它会先查询一级缓存,一级缓存未命中时,才会查询数据库。最后还会将得到的结果对象放入 TransactionalCache.entriesToAddOnCommit
集合中保存。
CachingExecutor.query()
方法的具体代码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建CacheKey对象
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取查询语句所在命名空间对应的二级缓存
Cache cache = ms.getCache();
// :是否开启了二级缓存功能
if (cache != null) {
// 根据<select>节点的配置,决定是否需妥清空二级缓存
flushCacheIfRequired(ms);
// 检测SQL节点的useCache配置以及是否使用了resultHandler配置
if (ms.isUseCache() && resultHandler == null) {
// 二级缓存不能保存输出类型的参数 如果查询操作调用了包含输出参数的存储过程,则报错
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 查询二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 二级缓存没有相应的结果对象,调用封装的Executor对象的query()方法
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将查询结果保存到TransactionalCache.entriesToAddOnCommit集合中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有启动二级缓存,调用封装的Executor对象的query()方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看 CachingExecutor.commit()
和 rollback()
方法的实现,它们首先调用底层的 Executor
对象的对应方法完成提交和回滚,然后调用 TransactionalCacheManager
的对应方法完成对二级缓存的操作。具体实现如下:
@Override
public void commit(boolean required) throws SQLException {
// 调用底层的Executor提交事务
delegate.commit(required);
// 遍历相关TransactionalCache对象执行commit()方法
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
// 调用底层的Executor回滚事务
delegate.rollback(required);
} finally {
if (required) {
// 遍历相关TransactionalCache对象执行rollback()方法
tcm.rollback();
}
}
}
StatementHandler
StatementHandler
接口中的功能很多,例如创建 Statement
对象,为 SQL
语句绑定实参,执行 select、insert、update、delete
等多种类型的 SQL
语句,批量执行 SQL
语句,将结果集映射成结果对象。
StatementHandler
接口的定义如下:
public interface StatementHandler {
// 从连接中获取一个Statement
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
// 参数绑定
void parameterize(Statement statement)
throws SQLException;
// 批量执行
void batch(Statement statement)
throws SQLException;
// 执行update、insert、delete操作
int update(Statement statement)
throws SQLException;
// 执行select操作
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
RoutingStatementHandler
RoutingStatementHandler
会根据 MappedStatement
指定的 statementTyp
字段创建对应的 StatementHandler
接口实现。RoutingStatementHandler
类的具体实现如下:
public class RoutingStatementHandler implements StatementHandler {
// 封装的真正的StatementHandler对象
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 根据MappedStatement的配置,生成一个对应的StatementHandler对象,并设置到delegate字段中
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
//...通过delegate调用对应的方法
}
BaseStatementHandler
BaseStatementHandler
是一个实现了 StatementHandler
接口的抽象类,它只提供了一些参数绑定相关的方法,并没有实现操作数据库的方法。BaseStatementHandler
字段的含义如下:
// 记录使用的ResultSetHandler对象,将结果集映射成结果对象
protected final ResultSetHandler resultSetHandler;
// 记录使用的ParameterHandler对象,使用传入的实参替换SQL语句的中的?占位符
protected final ParameterHandler parameterHandler;
// 记录执行SQL语句的Executor对象
protected final Executor executor;
// 记录SQL语句对应的MappedStatement、BoundSql对象
protected final MappedStatement mappedStatement;
// RowBounds记录了用户设置的offset,limit,用于在结果集中定位映射的起始位置和结束位置
protected final RowBounds rowBounds;
在 BaseStatementHandler
的构造方法中,除了初始化上述字段之外,还会调用 KeyGenerator.processBefore()
方法初始化 SQL
语句的主键,具体实现如下:
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// ...其他字段忽略
if (boundSql == null) {
// 调用keyGenerator.processBefore()方法获取主键
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
}
protected void generateKeys(Object parameter) {
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
ErrorContext.instance().store();
keyGenerator.processBefore(executor, mappedStatement, null, parameter);
ErrorContext.instance().recall();
}
BaseStatementHandler
实现了 StatementHandler
接口中的 prepare()
方法,该方法首先调 instantiateStatement()
抽象方法初始化 Java.sqI.Statement
对象 然后为其配置超时时间以及 fetchSize
等设置。
ParameterHandler
通过前面对动态 SQL
的介绍可知,在 BoundSql
中记录的 SQL
语句中可能包含 ?
占位符,而每个 ?
占位符都对应了 BoundSql、parameterMappings
集合中的一个元素,在该 ParameterMapping
中记录了对应参数名称以及该参数的相关属性。
在 ParameterHandler
接口中只定义了一个 setParameters()
方法,该方法主要负责调用 PreparedStatement.set*()
方法为 SQL
语句绑定实参。MyBatis
只为 ParameterHandler
接口提供了一个实现类 DefaultParameterHandler.DefaultParameterHandler
中核心字段的含义如下:
// 管理mybatis中的全部TypeHandler对象
private final TypeHandlerRegistry typeHandlerRegistry;
// 其中记录SQL节点相应的配置信息
private final MappedStatement mappedStatement;
// 用户传入的实参对象
private final Object parameterObject;
// 对应的BoundSql对象,需要设置参数的PreparedStatement对象
private final BoundSql boundSql;
在 DefaultParameterHandler.setParameters()
方法中会遍历 BoundSql.parameterMappings
集合中记录的 ParameterMapping
对象,井根据其中记录的参数名称查找相应实参,然后与 SQL
语句绑定。setParameters()
方法的具体实现如下:
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 取出sql中的参数映射列表
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 过滤掉存储过程中的输出参数
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
// 获取对应的实参值
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
// 实参为空
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 根据TypeHandler转换成JdbcType
value = parameterObject;
} else {
// 获取对象中相应的属性位或查找Map对象中值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获取ParameterMapping中设置的TypeHandler对象
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// //通过TypeHandler.setParameters()方法会调用PreparedStatement.set*()方法为SQL语句绑定相应的实参
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
为 SQL
语句绑定完实参之后,就可以调用 Statement
对象相应的 execute()
方法,将 SQL
语句交给数据库执行。
SimpleStatementHandler
SimpleStatementHandler
继承了BaseStatementHandler
抽象类。它底层使用 java.sql.Statement
对象来完成数据库的相关操作,所以 SQL
语句中不能存在占位符相应的,SimpleStatementHandler.parameterize()
方法是空实现。
SimpleStatementHandler.instantiateStatement()
方法直接通过 JDBCConnection
创建 Statement
对象,具体实现如下:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.createStatement();
} else {
// 设置结果集是否可以滚动及其游标是否可以上下移动,设置结果集是否可更新
return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
上面创建的 Statement
对象之后会被用于完成数据库操作,SimpleStatementHandler.query()
方法等完成了数据库查询的操作,并通过 ResultSetHandler
将结果集映射成结果对象。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// 获取SQL语句
String sql = boundSql.getSql();
// 执行SQL语句
statement.execute(sql);
// 映射结果集
return resultSetHandler.handleResultSets(statement);
}
SimpleStatementHandler
中的 queryCursor()、batch()
方法与 query()
方法实现类似,也是直接调用 Statement
对象的相应方法,不再赘述。
SimpleStatementHandler.update()
方法负责执行 insert、update、delete
等类型的 SQL
语句,并且会根据配置的 KeyGenerator
获取数据库生成的主键 具体实现如下:
@Override
public int update(Statement statement) throws SQLException {
// 获取SQL语句
String sql = boundSql.getSql();
// 获取实参对象
Object parameterObject = boundSql.getParameterObject();
// 获取配置的KeyGenerator对象
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
int rows;
if (keyGenerator instanceof Jdbc3KeyGenerator) {
// 执行SQL语句
statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
// 获取受影响的行
rows = statement.getUpdateCount();
// 将数据库生成的主键添加到parameterObject中
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else if (keyGenerator instanceof SelectKeyGenerator) {
// 执行SQL语句
statement.execute(sql);
// 获取受影响的行
rows = statement.getUpdateCount();
// 执行<selectKey>节点中配置的SQL语句获取数据库生成的主键,添加到parameterObject中
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else {
statement.execute(sql);
rows = statement.getUpdateCount();
}
return rows;
}
PreparedStatementHandler
PreparedStatementHandler
底层依赖于 java.sql.PreparedStatement
对象来完成数据库的相关操作。在 SimpleStatementHandler.parameterize()
方法中, 会调用前面介绍的 ParameterHandler.setParameters()
方法完成 SQL
语句的参数绑定。
PreparedStatementHandler.instantiateStatement()
方法直接调用 JDBC Connection
的 prepareStatement()
方法创建 PreparedStatement
对象, 具体实现如下:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
// 获取SQL语句
String sql = boundSql.getSql();
// 获取mappedStatement.getKeyGenerator()创建prepareStatement对象
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
// insert语句执行完成之后,会将keyColumnNames指定的列返回
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
// 设置结果集是否可以滚动以及其游标是否可以上下移动,设置结果集是否可更新
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
PreparedStatementHandler
中其他方法的实现与 SimpleStatementHandler
对应方法的实现类似,这里就不再赘述。
CallableStatementHandler
CallableStatementHandler
底层依赖于 java.sql.CallableStatement
存储过程 parameterize()
方法也会调用 ParameterHandler.setParameters()
方法完成 SQL
语句的参数绑定指定输出参数的索引位置和 JDBC
类型。其余方法与前面介绍 SimpleStatementHandler
实现类似,唯一区别是会调用 resultSetHandler.handleOutputParameters()
处理输出参数。
总结
本文从一个简单的 MyBatis
示例带领大家从源码的角度分析 MyBatis
的执行流程。希望大家在使用 MyBatis
时知其所以然,同时也帮助大家在使用 MyBatis
时遇到问题有方向去排查。感谢大家的阅读。
参考文章
转载自:https://juejin.cn/post/7158778901999648804