Mybatis执行SQL流程(六)之Executor执行器
文章目录
- 执行器
- BaseExecutor
- SimpleExecutor
- ReuseExecutor
- BatchExecutor
- 如何指定执行器类型
- 源码
- insert操作
- select
执行器
BaseExecutor
执行器的抽象基类
1、模板方法模式
定义了执行器的通用骨架(如查询/更新流程、事务提交/回滚、本地缓存管理),将具体操作延迟到子类实现(SimpleExecutor、ReuseExecutor、BatchExecutor)
2、一级缓存(Local Cache)管理
默认开启,缓存范围限定在SqlSession 生命周期内(即会话级缓存),通过PerpetualCache实现
SimpleExecutor
- 工作原理
-
- 每次执行 select 或 update 操作时,都会创建一个新的 PreparedStatement 对象。
- 执行完毕后,立即关闭这个Statement 对象(以及关联的游标ResultSet)
- 特点
-
- 简单直接:逻辑清晰,没有复杂的缓存或复用机制
- 资源开销大:频繁创建和销毁Statement 对象会带来额外的开销,尤其是在循环中多次执行SQL时。
- 无重用:相同的SQL语句每次都会创建新的PreparedStatement。
- 适用场景:对性能要求不高、SQL执行不频繁或每次SQL都不同的简单应用。作为默认选项,它最不容易出错。
ReuseExecutor
- 工作原理
-
- 在同一个SqlSession 的生命周期内,会将执行过的PreparedStatement 缓存起来(以SQL语句本身作为Key)
- 当再次执行完全相同的SQL 语句(字符串内容相同)时,会从缓存中取出并复用这个PreparedStatement。
- 不会在执行后立即关闭Statement,而是在SqlSession 关闭时统一关闭所有缓存的Statement。
- 特点
-
- 减少创建开销:避免了重复编译相同的SQL 语句,提高了执行相同 SQL 的效率
- 节省资源:复用PreparedStatement 减少了数据库驱动创建对象的开销
- 潜在内存泄漏风险:如果 SqlSession 生命周期很长且执行了大量不同的SQL,缓存会持续增长,占用内存。
- 依赖 SQL 字符串匹配:必须SQL字符串(包括空格、换行)完全一致才能复用
- 适用场景:在同一个 SqlSession 中需要反复执行完全相同SQL 语句的场景(例如循环插入大量数据,但每条SQL 都相同)。相比 BatchExecutor 灵活性稍高
BatchExecutor
- 工作原理
-
- 针对update 和 insert 语句(select会退化为SimpleExecutor行为)
- 不会立即执行每一条SQL。而是将多个update/insert 语句添加到一个批处理队列中
- 当调用 commit(), flushStatement() 或执行一个 select 语句(会触发自动flush)时,一次性将队列中的所有语句发送到数据库执行(使用jdbc 的 addBatch() 和 executeBatch())
- 对于select语句,他会立即执行,因为它无法批量处理查询
- 特点
-
- 大幅提升批量操作性能:将多次网络往返减少为1次(或少量几次),这是最主要的优势
- 必须显示提交/刷新:开发者需要理解并正确使用 commit 或 flushStatements 来确保 SQL 真正执行到数据库。忘记提交会导致操作丢失。
- 事务边界:通常与显示事务管理结合使用,批量操作要么全部重构,要么全部失败(取决于数据库和事务设置)
- 内存管理:大量未提交的批处理操作会占用JDBC驱动和数据库的资源。
- 适用场景:需要高效执行大量 insert 或 update 操作的场景。
如何指定执行器类型
方法一:
在创建SqlSession时,通过 SqlSessionFactory.openSession()
方法的重载版本指定 ExecutorType
参数:
// 使用默认的 SimpleExecutor (等同于 openSession())
SqlSession session = sqlSessionFactory.openSession();// 显式指定为 ReuseExecutor
SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE);// 显式指定为 BatchExecutor (通常需要同时指定事务控制)
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 对于 BatchExecutor, 通常需要手动提交
try {YourMapper mapper = session.getMapper(YourMapper.class);for (YourObject obj : list) {mapper.insert(obj); // 此时只是加入批处理队列,未执行}session.commit(); // 或 session.flushStatements(); 真正执行批处理
} finally {session.close();
}
实践:
@ResourceSqlSessionFactory sqlSessionFactory;@GetMapping("/{id}")public User getUserById(@PathVariable Long id) {SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.getUserById(id);sqlSession.close();return user;}
方法二:
在mybatis-config.xml文件中配置全局执行类型
<configuration><settings><!-- 设置默认执行器类型 --><setting name="defaultExecutorType" value="BATCH"/> </settings>
</configuration>
或者application.yml中配置:
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.zy.entityconfiguration:map-underscore-to-camel-case: truecache-enabled: truedefault-executor-type: batch # 设置执行器类型
方法三:接入springboot项目
// 可以配置多个模板
@Bean(name = "batchSqlSessionTemplate")
public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}@Bean(name = "reuseSqlSessionTemplate")
public SqlSessionTemplate reuseSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.REUSE);
}// 使用时注入
@Service
public class UserService {@Autowired @Qualifier("batchSqlSessionTemplate") // 注入批量模板private SqlSessionTemplate batchSqlSessionTemplate;public void batchInsert(List<User> users) {UserMapper mapper = batchSqlSessionTemplate.getMapper(UserMapper.class);for (User user : users) {mapper.insert(user);}batchSqlSessionTemplate.commit(); // 提交批处理}
}
不做配置时默认为Simple。
源码
从上一篇文里面了解到:MapperMethod
中执行对方法进行了区分,Sqlsession调用 Executor 对应的方法执行sql。以SimpleExecutor的源码了解:
insert操作
// BaseExecutor.java
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}clearLocalCache(); // 清空一级缓存return doUpdate(ms, parameter); // 子类重写
}
// SimpleExecutor 重写的方法,例如执行 sql:@Insert("insert into user(id, username) values (#{id}, #{username})")
// 大致流程如下:
doUpdate-> prepareStatement-> getConnection-> handler.prepare // 通过connection 实例化一个Statement对象-> handler.paramterize // 通过typeHandler为PreparedStatement对象设置数据,即如使用jdbc那样为PreparedStatement更新对应占位符对应的数据-> handler.update-> PreparedStatement.execute() // 执行sql-> keyGenerator.processAfter // 前面设置的KeyGenerator processAfter方法此处生效// SimpleExecutor.java:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); // 此处boundSql传参为null,在初始化StatementHandler中,会根据KeyGenerator类型来实例化boundSql,详细内容见KeyGeneratorstmt = prepareStatement(handler, ms.getStatementLog()); // 实例化PrepareStatement对象,并且对占位符设置参数return handler.update(stmt);} finally {closeStatement(stmt);}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog); // 项目中与Springboot进行结合,配置了DataSource,若未指定哪一种数据库连接池管理的话,则默认使用Hikaristmt = handler.prepare(connection, transaction.getTimeout()); // 使用StatementHandler实例化Statement对象handler.parameterize(stmt);return stmt;
}
// BaseStatementHandler 的 prepare 方法
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {ErrorContext.instance().sql(boundSql.getSql());Statement statement = null;try {statement = instantiateStatement(connection); // 源码见下文setStatementTimeout(statement, transactionTimeout);setFetchSize(statement);return statement;} catch (SQLException e) {closeStatement(statement);throw e;} catch (Exception e) {closeStatement(statement);throw new ExecutorException("Error preparing statement. Cause: " + e, e);}
}
// 以 PreparedStatementHandler 为例
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {String sql = boundSql.getSql();if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {String[] keyColumnNames = mappedStatement.getKeyColumns();if (keyColumnNames == null) {return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);} else {return connection.prepareStatement(sql, keyColumnNames);}} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {// 涉及到ResultSet类型,后续补充。当执行前面的insert语句时,进入该分支return connection.prepareStatement(sql); // hikari还为连接做了一层代理。在项目中配置的Mysql数据库,即本质上还是Mysql的connection实现类执行方法} else {return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);}
}
// 以 PreparedStatementHandler 为例:对Statement中的占位符进行处理
@Override
public void parameterize(Statement statement) throws SQLException {parameterHandler.setParameters((PreparedStatement) statement);
}
// DefaultParameterHandler 中的 方法
@Override
public void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());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 paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 存在参数类型对应的TypeHandlervalue = parameterObject;} else {// 此处逻辑较为复杂,MetaObject后续在KeyGenerator中也使用到,此处简单说明,后续详细说明。/** 执行的接口为:@Insert("insert into user(id, username) values (#{id}, #{username})")int insertUser(User user);入参为自定义Bean,即parameterObject类型为User类型。要获取User对象的id、username属性值,通过反射调用User的getId(),getUsername()方法来获取对应的属性值。*/MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {typeHandler.setParameter(ps, i + 1, value, jdbcType); // 使用 TypeHandler 为PreparedStatement中的占位符替换数据。TypeHandler 的详细内容见 TypeHandler 环节} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}
}
预处理完成后,接着就是执行sql
// 以 PreparedStatementHandler 为例
@Override
public int update(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute(); // 执行sqlint rows = ps.getUpdateCount();Object parameterObject = boundSql.getParameterObject();KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); // 执行KeyGenerator的processAfter方法。上述sql中不需要执行KeyGeneratorreturn rows;
}
select
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);// 涉及到二级缓存查询。缓存查询流程见二级缓存环节return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// 此处越过二级缓存,进入数据库查询流程
// SimpleExecutor.java@Overridepublic <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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler); // 使用StatementHandler执行查询。由子类重写该方法} finally {closeStatement(stmt);}}
// 以PreparedStatementHandler为例
@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute(); // 执行sqlreturn resultSetHandler.handleResultSets(ps);// 处理查询的结果集。内容还有点多,后续再单独补充吧}
总结:
对于查询来说,主要涉及到二级缓存、一级缓存、参数处理、结果集处理。
对于新增来说,涉及到主键自动生成keyGenerator、一级缓存清除、参数处理、typeHandler。
其中参数处理时,使用 MetaObject metaObject获取对应属性值;结果集处理时使用 MetaObject metaObject设置属性值。参数处理时都包含了对TypeHandler的使用。
KeyGenerator包含sql执行前获取主键、执行后获取主键。
真正执行sql是交给jdbc执行的。