640.png

StatementHandler

StatementHandler 接口是 MyBatis 的核心接口之一,它完成了 MyBatis 中最核心的工作,也是 Executor 接口实现的基础。该接口中的功能很多,例如创建 Statement 对象,为 SQL 语句绑定实参,执行 select、insert、update、delete 等多种类型的 SQL 语句。

  1. public interface StatementHandler {
  2. // 从连接中获取一个Statement
  3. Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
  4. // 绑定statement执行时所需的实参
  5. void parameterize(Statement statement) throws SQLException;
  6. // 批量执行sql语句
  7. void batch(Statement statement) throws SQLException;
  8. // 执行update、insert、delete语句
  9. int update(Statement statement) throws SQLException;
  10. // 执行select语句
  11. <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
  12. ......
  13. }

MyBatis 中提供了 StatementHandler 接口的多种实现类,具体如下图所示:
image.png

1. RoutingStatementHandler

RoutingStatementHandler 在构造函数中会根据 MappedStatement 中指定的 statementType 字段,创建对应的 StatementHandler 接口实现。

  1. public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  2. // RoutingStatementHandler的主要功能就是根据MappedStatement的配置生成一个对应的StatementHandler对象,并设置到delegate字段中
  3. switch (ms.getStatementType()) {
  4. case STATEMENT:
  5. delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  6. break;
  7. case PREPARED:
  8. delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  9. break;
  10. case CALLABLE:
  11. delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  12. break;
  13. default:
  14. throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  15. }
  16. }

在 RoutingStatementHandler 的其他方法中,都会委托给底层的 delegate 对象来完成具体的逻辑。

2. BaseStatementHandler

BaseStatementHandler 是一个实现了 StatementHandler 接口的抽象类,它只提供了一些参数绑定相关的方法,并没有实现操作数据库的方法。其核心字段如下:

  1. // 主要将结果集映射成结果对象
  2. protected final ResultSetHandler resultSetHandler;
  3. // 用于为sql语句绑定实参,也就是使用传入的实参替换sql语句中"?"占位符
  4. protected final ParameterHandler parameterHandler;
  5. // 用于执行sql语句的Executor对象
  6. protected final Executor executor;
  7. // 记录sql语句对应的MapperStatement和BoundSql对象
  8. protected final MappedStatement mappedStatement;
  9. protected BoundSql boundSql;
  10. // 该对象记录了用户设置的offset和limit,用于在结果集中定位映射的起始位置和结束位置
  11. protected final RowBounds rowBounds;

在 BoundSql 中记录的 SQL 语句可能包含 “?” 占位符。为此,MyBatis 会通过 ParameterHandler 来为 SQL 语句绑定实参,为 SQL 语句绑定完实参之后,就可以调用 Statement 对象相应的 execute() 方法,将 SQL 语句交给数据库执行了。

2.1 SimpleStatementHandler

SimpleStatementHandler 继承了 BaseStatementHandler 抽象类。它底层使用 java.sql.Statement 对象来完成数据库的相关操作,所以 SQL 语句中不能存在占位符。它的 instantiateStatement() 方法直接通过 JDBC Connection 来创建 Statement 对象,具体实现如下:

  1. protected Statement instantiateStatement(Connection connection) throws SQLException {
  2. if (mappedStatement.getResultSetType() != null) {
  3. // 设置结果集是否可以滚动及其游标是否可以上下移动,设置结果集是否可更新
  4. return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  5. } else {
  6. return connection.createStatement();
  7. }
  8. }

上面创建的 Statement 对象之后会被用于完成数据库操作,SimpleStatementHandler.query() 方法等完成了数据库查询的操作,并通过 ResultSetHandler 将结果集映射成结果对象。

  1. public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  2. // 获取SQL语句
  3. String sql = boundSql.getSql();
  4. // 执行SQL语句
  5. statement.execute(sql);
  6. // 映射结果集
  7. return resultSetHandler.<E>handleResultSets(statement);
  8. }

SimpleStatementHandler 的 update() 方法负责执行 insert、update 或 delete 等类型的 SQL 语句,并且会根据配置的 KeyGenerator 获取数据库生成的主键。

2.2 PreparedStatementHandler

PreparedStatementHandler 是最常用的 StatementHandler 实现,它同样继承了 BaseStatementHandler 抽象类。它的底层依赖于 java.sql.PreparedStatement 对象来完成数据库的相关操作,在其 parameterize() 方法中会完成 SQL 语句的参数绑定,所以其维护的 SQL 语句是可以包含 “?” 占位符的。

PreparedStatementHandler.instantiateStatement() 方法直接调用 JDBC Connection 的 prepareStatement()方法创建 PreparedStatement 对象,具体实现如下:

  1. protected Statement instantiateStatement(Connection connection) throws SQLException {
  2. // 获取待执行的SQL语句
  3. String sql = boundSql.getSql();
  4. if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
  5. String[] keyColumnNames = mappedStatement.getKeyColumns();
  6. if (keyColumnNames == null) {
  7. // 返回数据库生成的主键
  8. return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
  9. } else {
  10. // 在insert语句执行完成之后,会将keyColumnNames指定的列返回
  11. return connection.prepareStatement(sql, keyColumnNames);
  12. }
  13. } else if (mappedStatement.getResultSetType() != null) {
  14. // 设置结果集是否可以滚动以及其游标是否可以上下移动,设置结采集是否可更新
  15. return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  16. } else {
  17. // 创建普通的 PreparedStatement 对象
  18. return connection.prepareStatement(sql);
  19. }
  20. }

PreparedStatementHandler 的 query() 方法、batch() 方法以及 update() 方法与 SimpleStatementHandler 的实现基本相同,只不过是把 Statement API 换成了 PrepareStatement API 而已。

Executor

Executor 是 MyBatis 的核心接口之一, 其中定义了数据库操作的基本方法。在实际应用中经常涉及的 SqISession 接口的功能,都是基于 Executor 接口实现的。Executor 接口中定义的方法如下:

  1. public interface Executor {
  2. // 执行update、insert、delete三种类型的sql语句
  3. int update(MappedStatement ms, Object parameter) throws SQLException;
  4. // 执行select类型的sql语句
  5. <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  6. // 批量执行sql语句
  7. List<BatchResult> flushStatements() throws SQLException;
  8. void commit(boolean required) throws SQLException;
  9. void rollback(boolean required) throws SQLException;
  10. Transaction getTransaction();
  11. void close(boolean forceRollback);
  12. ......
  13. }

MyBatis 提供的 Executor 接口实现如下图所示:
image.png

1. BaseExecutor

BaseExecutor 是一个实现了 Executor 接口的抽象类,它实现了 Executor 接口的大部分方法,主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor 的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别是:doUpdate()、doQuery()、doQueryCursor()、doFlushStatement() 方法,其余的功能在均 BaseExecutor 中实现。

BaseExecutor 中各个字段的含义如下:

  1. // 实现事务的提交、回滚和关闭操作
  2. protected Transaction transaction;
  3. // 封装的Executor对象
  4. protected Executor wrapper;
  5. // 延迟加载队列
  6. protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  7. // 一级缓存,用于缓存该Executor对象查询结果集映射得到的结果对象
  8. protected PerpetualCache localCache;
  9. // 一级缓存,用于缓存输出类型的参数
  10. protected PerpetualCache localOutputParameterCache;

1.1 一级缓存

一级缓存是会话级别的缓存,在 MyBatis 中每创建一个 SqlSession 对象,就表示开启一次数据库会话。在一次会话中,应用程序可能会在短时间内,例如一个事务内,反复执行完全相同的查询语句,如果不对数据进行缓存,那么每一次查询都会执行一次数据库查询操作,而多次完全相同的、时间间隔较短的查询语句得到的结果集极有可能完全相同,这也就造成了数据库资源的浪费。

MyBatis 中的 SqlSession 是通过 Executor 对象完成数据库操作的,在 Executor 对象中建立了一个简单的缓存,也就是一级缓存,它会将每次查询的结果对象缓存起来。在执行查询操作时,会先查询一级缓存,如果其中存在完全一样的查询语句,则直接从一级缓存中取出相应的结果对象返回给用户,避免访问数据库,从而减小了数据库的压力。一级缓存默认是开启的,一般情况下,不需要用户进行特殊配置。

一级缓存的生命周期与 SqlSession 相同,其实也就与 SqISession 中封装的 Executor 对象的生命周期相同。当调用 Executor 对象的 close() 方法时,该 Executor 对象对应的一级缓存就变得不可用。一级缓存中对象的存活时间受很多方面的影响,例如,在调用 Executor.update() 方法时,会先清空一级缓存。

下图为 BaseExecutor.query() 方法的实现思路:
image.png
BaseExecutor.update() 方法负责执行 insert、update、delete 三类 SQL 语句,它是调用 doUpdate() 模板方法实现的。在调用 doUpdate() 方法前会清空一级缓存,因为执行 SQL 语句后,数据库中的数据已经更新,一级缓存的内容与数据库中的数据可能已经不一致了。

1.2 事务相关操作

在 BatchExecutor 实现中,可以缓存多条 SQL 语句,等待合适的时机将缓存的多条 SQL 语句一并发送到数据库执行。Executor.flushStatements() 方法主要是针对批处理多条 SQL 语句的,在 commit()、rollback() 等方法中都会先调用 flushStatements() 方法,然后再执行相关事务操作。

BaseExecutor.commit() 方法首先会清空一级缓存、调用 flushStatements() 方法,最后才根据参数决定是否真正提交事务。

  1. public void commit(boolean required) throws SQLException {
  2. // 判断当前 Executor 是否已经关闭
  3. if (closed) {
  4. throw new ExecutorException("Cannot commit, transaction is already closed");
  5. }
  6. // 清空一级缓存
  7. clearLocalCache();
  8. // 执行缓存的 SQL 语句
  9. flushStatements();
  10. // 根据 required 参数决定是否提交事务
  11. if (required) {
  12. transaction.commit();
  13. }
  14. }

BaseExecutor.rollback() 方法的实现与 commit() 实现类似,同样会根据参数决定是否真正回滚事务,区别是其中调用的是 flushStatements() 方法的 isRollBack 参数为 true,这就会导致 Executor 中缓存的 SQL 语句全部被忽略。

2. SimpleExecutor

SimpleExecutor 继承了 BaseExecutor 抽象类,是最简单的 Executor 接口实现。先来看下 doQuery() 方法的具体实现:

  1. public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  2. Statement stmt = null;
  3. try {
  4. // 获取配置对象
  5. Configuration configuration = ms.getConfiguration();
  6. // 创建StatementHandler对象,实际返回的是 RoutingStatementHandler 对象
  7. StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  8. // 完成Statement的创建和初始化,该方法首先会调用StatementHandler.prepare()方法创建Statement对象
  9. // 然后调用StatementHandler.parameterize()方法处理占位符
  10. stmt = prepareStatement(handler, ms.getStatementLog());
  11. // 调用StatementHandler.query()方法执行SQL语句,并通过 ResultSetHandler 完成结果集的映射
  12. return handler.<E>query(stmt, resultHandler);
  13. } finally {
  14. closeStatement(stmt);
  15. }
  16. }

3. ReuseExecutor

在传统 JDBC 编程中,重用 Statement 对象是常用的一种优化手段,该优化手段可以减少 SQL 预编译的开销以及创建和销毁 Statement 对象的开销,从而提高性能。ReuseExecutor 提供了 Statement 重用的功能,它通过 statementMap 字段(HashMap类型)缓存使用过的 Statement 对象,key 是 SQL 语句,value 是 SQL 对应的 Statement 对象。

ReuseExecutor.doQuery()、doQueryCursor()、doUpdate() 方法的实现与 SimpleExecutor 中对应方法的实现一样,区别在于其中调用的 preparestatement() 方法,SimpleExecutor 每次都会通过 JDBC Connection 创建新的 Statement 对象,而 ReuseExecutor 则会先尝试重用 StaternentMap 中缓存的 Statement 对象。

  1. private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  2. Statement stmt;
  3. BoundSql boundSql = handler.getBoundSql();
  4. // 获取 SQL 语句
  5. String sql = boundSql.getSql();
  6. // 检测是否缓存了相同模式的 SQL 语句所对应的 Statement 对象
  7. if (hasStatementFor(sql)) {
  8. // 获取statementMap集合中缓存的Statement对象
  9. stmt = getStatement(sql);
  10. // 修改超时时间
  11. applyTransactionTimeout(stmt);
  12. } else {
  13. // 获取数据库连接
  14. Connection connection = getConnection(statementLog);
  15. // 创建新的 Statement 对象,并缓存到 staternentMap 集合中
  16. stmt = handler.prepare(connection, transaction.getTimeout());
  17. putStatement(sql, stmt);
  18. }
  19. handler.parameterize(stmt);
  20. return stmt;
  21. }

4. BatchExecutor

应用系统在执行一条 SQL 语句时,会将 SQL 语句以及相关参数通过网络发送到数据库系统。对于频繁操作数据库的应用系统来说,如果执行一条 SQL 语句就向数据库发送一次请求,很多时间会浪费在网络通信上。使用批量处理的优化方式可以在客户端缓存多条 SQL 语句,并在合适的时机将多条 SQL 语句打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。注意,批量执行多条 SQL 语句时,每次向数据库发送的 SQL 语句条数是有上限的,如果超过这个上限,数据库会拒绝执行这些 SQL 语句并抛出异常。所以批量发送 SQL 语句的时机很重要。

BatchExecutor 实现了批处理多条 SQL 语句的功能,其中核心字段的含义如下:

  1. // 缓存多个Statement对象其中每个Statement对象中都缓存了多条SQL语句
  2. private final List<Statement> statementList = new ArrayList<Statement>();
  3. // 记录批处理的结果,BatchResult中通过updateCounts字段(int[]类型)记录每个Statement执行批处理的结果
  4. private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
  5. // 记录当前执行的SQL语句
  6. private String currentSql;
  7. // 记录当前执行的 MappedStatement 对象
  8. private MappedStatement currentStatement;

注意,JDBC 中的批处理只支持 insert、update、delete 等类型的 SQL 语句,不支持 select 类型。

5. CachingExecutor

CachingExecutor 是一个 Executor 接口的装饰器,它为 Executor 对象增加了二级缓存的相关功能。MyBatis 中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个,具体如下所示:

  • 首先是 mybatis-config.xml 配置文件中的 cacheEnabled 配置,它是二级缓存的总开关。只有当该配置设置为 true 时,后面两项的配置才会有效果,该值默认为 true。


  • 如果在映射配置文件中配置了 节点,则在解析时会为该映射配置文件指定的命名空间创建相应的 Cache 对象作为其二级缓存。如果配置了 节点,则在解析时会将当前映射配置文件指定的命名空间与 <cache-ref> 节点的 namespace 属性指定的命名空间共享同一个 Cache 对象。通过这两个节点的配置,用户可以在命名空间的粒度上管理二级缓存的开启和关闭。


  • 最后一个配置项是 <select> 节点中的 useCache 属性,该属性表示查询操作产生的结果对象是否要保存到二级缓存中。该属性默认是 true。

CachingExecutor 中的核心字段如下:

  1. // 执行数据库操作
  2. private final Executor delegate;
  3. // 缓存管理器
  4. private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 用于管理 CachingExecutor 使用的二级缓存对象,其中只定义了一个 transactionalCaches 字段(HashMap 类型),它的 key 是对应的 CachingExecutor 使用的二级缓存对象,value 是相应的 TransactionalCache 对象。TransactionalCache 继承了 Cache 接口,用于保存在某个 SqlSession 的某个事务中需要向某个二级缓存中添加的缓存数据。该缓存不会直接将结果对象记录到其封装的二级缓存中,而是暂时保存在 entriesToAddOnCommit 集合中,只有在事务提交时才会将这些结果对象从 entriesToAddOnCommit 集合添加到二级缓存中。

SqlSession

SqlSession 是 MyBatis 核心接口之一,也是 MyBatis 接口层的主要组成部分,对外提供 MyBatis 常用 API。MyBatis 提供了两个 SqlSession 接口的实现,如图所示。
image.png
在 MyBatis 中,SqlSessionFactory 负责创建 SqlSession 对象,其中只包含了多个 openSession() 方法的重载,可以通过其参数指定事务的隔离级别、底层使用 Executor 的类型以及是否自动提交事务等方面的配置。