mybatis一个方法执行多条sql_MyBatis 源码解析:SQL 语句的执行机制(下)

83d56b859ad6886c0787749306db8246.png
  • ReuseExecutor

ReuseExecutor 提供了对 Statement 对象重用的机制,以减少该对象创建和销毁,以及 SQL 预编译所带来的开销。ReuseExecutor 类中定义了一个ReuseExecutor#statementMap属性(如下),其中 key 为 SQL 语句,value 为对应的 Statement 对象,以此实现对 Statement 对象的复用。

/** 缓存 Statement 对象,key 为对应的 SQL 语句(带有 ? 占位符) */
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();

ReuseExecutor 中的方法实现也基本上沿用了同一套思路,仍然以ReuseExecutor#doQuery为例进行说明,该方法实现如下:

public <E> List<E> doQuery(MappedStatement ms,
                           Object parameter,
                           RowBounds rowBounds,
                           ResultHandler resultHandler,
                           BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    // 创建对应的 StatementHandler 对象
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 先尝试从缓存中获取当前 SQL 对应的 Statement 对象,缓存不命中则创建一个新的并缓存
    Statement stmt = this.prepareStatement(handler, ms.getStatementLog());
    // 执行数据库查询操作,以及结果集映射
    return handler.query(stmt, resultHandler);
}

上述方法与SimpleExecutor#doQuery的区别在于在获取 Statement 对象时会先尝试从本地缓存中获取,如果缓存不命中则会创建一个新的 Statement 对象,并更新缓存,实现位于ReuseExecutor#prepareStatement方法中:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 获取缓存的 Statement 对象
    if (this.hasStatementFor(sql)) {
        stmt = this.getStatement(sql);
        this.applyTransactionTimeout(stmt);
    }
    // 缓存不命中,新建一个 Statement 对象并缓存
    else {
        Connection connection = this.getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        this.putStatement(sql, stmt);
    }
    // 绑定实参
    handler.parameterize(stmt);
    return stmt;
}

BatchExecutor

BatchExecutor 用于批量执行 SQL 语句。通常应用程序都是单行的执行 SQL 语句,但是某些场景下单行执行数据库操作是比较耗时的,比如需要远程执行数据库操作。因此,JDBC 针对 INSERT、UPDATE,以及 DELETE 操作提供了批量执行的支持。

BatchExecutor 是批量 SQL 语句执行器,其属性定义如下:

/** 缓存多个 {@link Statement} 对象,每个对象都对应多条 SQL 语句 */
private final List<Statement> statementList = new ArrayList<>();
/** 记录批处理的结果,每个 {@link BatchResult} 对应一个 {@link Statement} 对象 */
private final List<BatchResult> batchResultList = new ArrayList<>();
/** 当前执行的 SQL 语句 */
private String currentSql;
/** 当前操作的 {@link MappedStatement} 对象 */
private MappedStatement currentStatement;

下面探究一下 BatchExecutor 的批处理执行过程。首先来看一下BatchExecutor#doUpdate方法实现,该方法用于添加批处理 SQL 语句:

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();
    final String sql = boundSql.getSql();
    final Statement stmt;
    // 当前执行的 SQL 语句与前一次执行的 SQL 语句(不包含实参)相同,且对应的 MappedStatement 对象也相同
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
        // 获取缓存的最后一个 Statement 对象,即前一次使用的 Statement 对象
        int last = statementList.size() - 1;
        stmt = statementList.get(last);
        this.applyTransactionTimeout(stmt);
        // 绑定实参
        handler.parameterize(stmt);//fix Issues 322
        BatchResult batchResult = batchResultList.get(last);
        batchResult.addParameterObject(parameterObject);
    }
    // 当前执行的 SQL 语句与前一次执行的 SQL 语句不同
    else {
        Connection connection = this.getConnection(ms.getStatementLog());
        // 获取一个新的 Statement 对象
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 绑定实参
        handler.parameterize(stmt);    //fix Issues 322
        // 记录本次执行的 SQL 语句和 MappedStatement 对象
        currentSql = sql;
        currentStatement = ms;
        // 缓存新建的 Statement 对象
        statementList.add(stmt);
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 基于 Statement#addBatch 方法添加批量 SQL 语句
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
}

上述方法中会判断当前执行的 SQL 模式(包含 ? 占位符的 SQL 语句)是否与前一次执行的相同,如果相同就会获取上次执行的 Statement 对象,并为之绑定实参;否则就会创建一个新的 Statement 对象,并记录本次执行的 SQL 模式,最后基于底层的数据库批处理方法 Statement#addBatch 添加批量 SQL 语句。由上述方法我们可以知道,对于连续同模式的批处理 SQL 操作会共享同一个 Statement 对象。

那么这些添加的批量 SQL 又是如何被执行的呢?这个过程位于 BatchExecutor#doFlushStatements 方法中,方法如下:

public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
        // 用于存储批量处理结果
        List<BatchResult> results = new ArrayList<>();
        if (isRollback) {
            return Collections.emptyList();
        }
        // 遍历处理缓存的 Statement 集合
        for (int i = 0, n = statementList.size(); i < n; i++) {
            Statement stmt = statementList.get(i);
            this.applyTransactionTimeout(stmt);
            BatchResult batchResult = batchResultList.get(i);
            try {
                // 批量执行当前 Statement 蕴含的多条 SQL 语句,并记录每条 SQL 语句影响的行数
                batchResult.setUpdateCounts(stmt.executeBatch());
                MappedStatement ms = batchResult.getMappedStatement();
                List<Object> parameterObjects = batchResult.getParameterObjects();
                KeyGenerator keyGenerator = ms.getKeyGenerator();
                if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
                    // 获取数据库生成的主键,并记录到 parameterObjects 中
                    Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
                    jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
                } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
                    for (Object parameter : parameterObjects) {
                        keyGenerator.processAfter(this, ms, stmt, parameter);
                    }
                }
                // Close statement to close cursor #1109
                this.closeStatement(stmt);
            } catch (BatchUpdateException e) {
                // ... 省略异常处理
            }
            // 记录封装当前 Statement 对象执行结果的 batchResult 到集合中
            results.add(batchResult);
        }
        return results;
    } finally {
        // 关闭所有的 Statement 对象
        for (Statement stmt : statementList) {
            this.closeStatement(stmt);
        }
        currentSql = null;
        statementList.clear();
        batchResultList.clear();
    }
}

方法会遍历我们在 BatchExecutor#doUpdate 中构造的 Statement 集合,分别执行集合中蕴含的 Statement 对象,并将执行的结果记录到 BatchResult 对象中(说明:在 BatchExecutor#doUpdate 方法中已经为每个 Statement 对象构造好了一个空的 BatchResult 对象,记录在 BatchExecutor#batchResultList 集合中),最后将 BatchResult 对象封装到集合中返回。因为都是数据库更新一类的操作,所以这里没有复杂的结果集映射,只需要记录每一条 SQL 语句执行所影响的行数即可。

CachingExecutor

由前面 Executor 的继承关系我们可以看到,CachingExecutor 相对于其它 Executor 实现来说似乎有其特别之处。CachingExecutor 直接实现了 Executor 接口,实际上它是一个 Executor 装饰器,用于为 Executor 提供二级缓存支持。该接口的属性定义如下:

/** 装饰的 {@link Executor} 对象 */
private final Executor delegate;
/** 用于管理当前使用的二级缓存对象 */
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

其中第一个属性就是 CachingExecutor 具体修饰的 Executor 对象。我们来看一下第二个属性,TransactionalCacheManager 用来管理当前 CachingExecutor 对应的二级缓存对象,它的方法实现都比较简单,其中相对让人疑惑的是它的唯一一个属性:

/** key 为对应的 {@link CachingExecutor} 使用的二级缓存对象,value 为采用 {@link TransactionalCache} 装饰的二级缓存对象 */
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

该属性的 key 就是当前对应的二级缓存,而 value 则是对于该二级缓存对象采用 TransactionalCache 装饰后的对象。所以 key 和 value 本质上都映射到同一个缓存对象,只是 value 采用了 TransactionalCache 进行增强。TransactionalCache 也是一个缓存装饰器,在前面介绍缓存装饰器实现时特意留着没有说明,这里一起来分析一下。该装饰器的属性定义如下:

/** 被装饰的 {@link Cache} 对象(二级缓存) */
private final Cache delegate;
/** 是否在提交事务时清空缓存 */
private boolean clearOnCommit;
/** 用于缓存数据,当提交事务时会将其中的数据写入二级缓存 */
private final Map<Object, Object> entriesToAddOnCommit;
/** 缓存未命中的 key */
private final Set<Object> entriesMissedInCache;

对应的读缓存和写缓存操作,以及事务提交方法实现比较简单,读者可以自行阅读源码。

继续回来看 CachingExecutor 的实现,所有实现方法中只有 CachingExecutor#query 方法稍微复杂一些,该方法的实现如下:

public <E> List<E> query(MappedStatement ms,
                         Object parameterObject,
                         RowBounds rowBounds,
                         ResultHandler resultHandler) throws SQLException {
    // 获取对应的 BoundSql 对象,并创建对应的 CacheKey
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 调用重载的 query 方法
    return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

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) {
        // 依据配置决定是否清空二级缓存
        this.flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            // 确保不是存储过程输出类型的参数
            this.ensureNoOutParams(ms, boundSql);
            // 查询二级缓存
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 二级缓存不命中,执行一级缓存查询,再不命中就查询数据库
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 缓存到 TransactionalCache#entriesToAddOnCommit 中
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 未启用二级缓存,则查询一级缓存,再不命中就查询数据库
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

正如我们一开始对于 MyBatis 缓存结构设计描绘的那样,上述方法首先在二级缓存中进行检索,如果二级缓存不命中则会执行被装饰的 Executor 对象的 Executor#query 方法。由前面的分析我们知道,Executor 的实现都自带一级缓存特性,所以接下去会查询一级缓存。只有在一级缓存也不命中的情况下,请求才会落库,并由数据库返回的结果对象更新一级缓存和二级缓存。

那么这里使用的二级缓存对象是在哪里创建的呢?实际上前面我们就定义说二级缓存是应用级别的,所以当应用启动时二级缓存就已经被创建了,这个过程发生在对映射文件进行解析时。在映射文件中我们会按照需要配置一定的 <cache/><cache-ref> 标签,而在解析 <cache/> 标签时会调用 MapperBuilderAssistant#useNewCache 方法创建对应的二级缓存对象。

总结

本文对 MyBatis 执行 SQL 语句所涉及到的各个方面做了一个比较详细的分析。当我们基于 MyBatis 触发一次数据库操作时,首先需要开启一次数据库会话,然后获取目标 Mapper 接口,并调用相应的 Mapper 方法执行数据库操作,最后拿到操作结果。MyBatis 在这中间基于动态代理机制实现了 SQL 语句的检索、参数绑定、数据库操作,以及结果集映射等一系列操作,并引入了缓存机制优化数据库查询性能。

回过头来看,MyBatis 的整体设计还是非常巧妙的,却也很是直观且简单,是对动态代理机制的典型应用,其设计思想和对于设计模式的应用值得我们在实际开发中借鉴。

本文是 MyBatis 源码解析系列的最后一篇文章,由于时间仓促,再加上作者水平有限,整个系列的文章中不免有错误之处,还望批评指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis 中,可以使用 `Mapper` 接口或者 `SqlSession` 直接执行多条 SQL 语句。 1. Mapper 接口方式 如果你使用的是 `Mapper` 接口方式,那么可以在 Mapper 接口中定义多个方法,每个方法对应一个 SQL 语句。然后在 Java 代码中调用这些方法即可执行多条 SQL 语句。 例如: ``` public interface UserMapper { void insertUser(User user); void updateUser(User user); void deleteUser(int userId); } ``` 在 Java 代码中,可以依次调用这三个方法执行多条 SQL 语句: ``` UserMapper mapper = sqlSession.getMapper(UserMapper.class); mapper.insertUser(user); mapper.updateUser(user); mapper.deleteUser(userId); ``` 2. SqlSession 方式 如果你使用的是 `SqlSession` 方式,那么可以调用 `SqlSession` 的 `selectList` 方法执行多条 SQL 语句。在这个方法中,可以传入一个包含多条 SQL 语句的 XML 文件,并且在每条 SQL 语句之间使用分号分隔。 例如,假设你的 XML 文件名为 `multiSql.xml`,内容如下: ``` <sql> INSERT INTO user (name, age) VALUES ('John', 25); </sql> <sql> UPDATE user SET name = 'Peter' WHERE age = 25; </sql> ``` 那么在 Java 代码中,可以这样调用 `selectList` 方法执行这两条 SQL 语句: ``` String statement = "multiSql"; sqlSession.selectList(statement); ``` 注意,在使用 `SqlSession` 执行多条 SQL 语句时,需要将 `ExecutorType` 设置为 `BATCH`,这样可以让 MyBatis 将多个 SQL 语句一起提交到数据库中,从而提高执行效率。例如: ``` SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值