Mybatis源码阅读(四):核心接口4.2——Executor(下)

*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新
*************************************优雅的分割线 **********************************
SimpleExecutor

接上一章博客继续。

SImpleExecutor继承了BaseExecutor类,是最简单的Executor实现。Executor使用了模板方法模式,所以SimpleExecutor不必在关心一级缓存等操作,只需要实现基本的4个方法。

首先看doQuery

/**
 * 执行查询操作
 * @param ms
 * @param parameter
 * @param rowBounds
 * @param resultHandler
 * @param boundSql
 * @param <E>
 * @return
 * @throws SQLException
 */
@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();
        // 创建RoutingStatementHandler,根据MappedStatement.statementType决定选择具体的StatementHandler
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 完成Statement的初始化。先创建对应的StatementHandler,再调用parameterize方法处理占位符
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 调用StatementHandler.query方法执行SQL
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

/**
 * 创建Statement并处理占位符
 * @param handler
 * @param statementLog
 * @return
 * @throws SQLException
 */
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // 创建Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 处理占位符
    handler.parameterize(stmt);
    return stmt;
}

代码很简单,先获取配置对象,再创建RoutingStatementHandler对象,对Statement进行初始化,最后调用query方法完成查询操作。doQueryCursor、update方法与之类似,不进行介绍。SimpleExecutor不提供批量处理SQL的功能所以doFlushStatement方法直接返回空集合。
ReuseExecutor

传统的JDBC编程中,Statement对象重用是最常见的一种优化手段,这样可以减少SQL的预编译以及创建、销毁Statement对象的开销,从而提高性能。

ReuseExecutor提供了Statement重用的功能,通过statementMap字段缓存使用过的Statement对象,key是sql,value是statement。

该类中的doQuery、doQueryCursor、doUpdate与SimpleExecutor中的实现一样, 不同的是PrepareStatement方法。SimpleExecutor每次都会通过JDBC的Connection创建新的Statement,而ReuseExecutor则先尝试从statementMap中查找缓存的对象。

/**
 * 默认会从缓存中查找Statement
 * @param handler
 * @param statementLog
 * @return
 * @throws SQLException
 */
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    //  获取SQL
    String sql = boundSql.getSql();
    if (hasStatementFor(sql)) {
        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方法中,都会调用doFlushStatement方法,所以在doFlushStatement方法中关闭Statement比较合适。方法实现如下。

@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {
    for (Statement stmt : statementMap.values()) {
        closeStatement(stmt);
    }
    statementMap.clear();
    return Collections.emptyList();
}

BatchExecutor

系统在执行一条sql语句的时候,会将SQQL语句以及相关参数通过网络发送到数据库。对于频繁操作数据库的系统,如果执行一条SQl就向数据库发送一次请求,在网络通信上会有很多性能折损。因此使用批量处理的方式进行优化,缓存多条SQL语句,在合适的时机将多条SQL打包发送给数据库执行,从而减少网络方面的开销,提高性能。

需要注意,批量执行SQL的时候,每次向数据库发送的SQL语句条数是有上限的,如果超出了这个上限,数据库会拒绝执行这些SQL并抛出异常。

BatchExecutor实现了批量处理SQL的功能,核心字段如下。

/**
 * 缓存多个Statement,每个Statement都缓存了多条SQL
 */
private final List<Statement> statementList = new ArrayList<>();
/**
 * 记录批处理的结果
 */
private final List<BatchResult> batchResultList = new ArrayList<>();
/**
 * 记录当前执行的SQL
 */
private String currentSql;
/**
 * 记录当前执行的MappedStatement
 */
private MappedStatement currentStatement;

JDBC只支持insert、update、delete的批处理,select不存在批处理一说,因此这里主要分析doUpdate方法。

doUpdate方法在添加一条SQL的时候,会先将currentSql字段记录的SQl以及currentStatement记录的MappedStatement对象与当前添加的SQL对比,如果相同则添加到同一个Statement对象中等待执行,不同则创建新的Statement对象并缓存到statementList集合中等待执行,代码如下。

@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    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相同,这里的sql还有问号占位符,并且MappedStatement和当前Statement相同,就添加到同一个Statement对象中等待执行
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
        // 获取最后一个Statement对象
        int last = statementList.size() - 1;
        stmt = statementList.get(last);
        applyTransactionTimeout(stmt);
        // 绑定实参,处理?占位符
        handler.parameterize(stmt);//fix Issues 322
        // 查找对应的BatchResult,记录用户传入的实参
        BatchResult batchResult = batchResultList.get(last);
        batchResult.addParameterObject(parameterObject);
    } else {
        // 否则创建新的Statement缓存到statementList中等待执行
        Connection connection = getConnection(ms.getStatementLog());
        // 创建Statement
        stmt = handler.prepare(connection, transaction.getTimeout());
        handler.parameterize(stmt);    //fix Issues 322
        currentSql = sql;
        currentStatement = ms;
        // 添加到statementList
        statementList.add(stmt);
        // 添加新的BatchResult
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
}

JDBC的Statement可以添加不同模式的SQL,每添加一个新模式的SQl就会触发一次编译操作。而PrepareStatement中只能添加统一模式的SQL语句,只触发一次编译操作,但是可以通过绑定多组不同的参数实现批处理。而BatchExecutor做的就是这件事,将连续添加的、相同模式的SQL语句添加到同一个Statement对象中,从而有效地减少编译次数。

在添加完待执行的SQL之后,doFlushStatement方法会处理这些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 stmt = statementList.get(i);
            applyTransactionTimeout(stmt);
            BatchResult batchResult = batchResultList.get(i);
            try {
                // 调用批量执行方法,返回int数组,每个元素都表示每条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
                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 {
        for (Statement stmt : statementList) {
            closeStatement(stmt);
        }
        currentSql = null;
        statementList.clear();
        batchResultList.clear();
    }
}

结语

Executor接口的内容有点多,因此就分成了两篇博客进行介绍,而最后的CachingExecutor是为Mybatis实现二级缓存功能,其中使用了装饰器模式。Mybatis的二级缓存功能在实际开发中很少会使用,因此这里就不进行介绍,感兴趣的朋友可以自己摸索。
*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新
*************************************优雅的分割线 **********************************

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值