深入认识MyBatis执行体系
本文主要讲解MyBatis的Executor的执行体系
Jdbc的执行过程
Jdbc常用的三种Statement
- Statement:一般不用了,只能处理静态sql
- PreparedStatement:可以对Sql预编译,防止Sql注入
- CallableStatement:执行存储过程(不过现在存储过程一般不推荐用)
ParparedStatement的执行过程
Jdbc的执行过程总体来说分为以下四步:
- 获取连接
- 预编译SQL
- 执行SQL
- 读取结果
图示如下:
关于防止sql注入的图示:
Jdbc常用的代码演示
代码准备
依赖
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
数据库脚本
create table student
(
id bigint auto_increment
primary key,
name varchar(50) null,
age int null
);
INSERT INTO student (id, name, age) VALUES (1, 'daxiong', 24);
INSERT INTO student (id, name, age) VALUES (2, '鲁班', 35);
数据库连接的创建代码
private Connection connection;
@Before
public void init() throws SQLException {
//DATASOURCE为数据库连接信息的常量类
connection = DriverManager.getConnection(DATASOURCE.URL, DATASOURCE.USER, DATASOURCE.PASSWORD);
}
@After
public void close() throws SQLException {
connection.close();
}
查询的简单例子
@Test
public void preparedStatementSelectTest() throws SQLException {
// 预编译
String selectSql = "SELECT name,age FROM student WHERE `name`=? ";
PreparedStatement preparedStatement = connection.prepareStatement(selectSql);
//添加参数
preparedStatement.setString(1,"daxiong");
//执行sql
preparedStatement.execute();
//获取结果集
ResultSet resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
String resultMsg = String.format("查询到的名字为:【%s】,age为:【%d】",
resultSet.getString(1),
resultSet.getInt(2));
System.out.println(resultMsg);
}
resultSet.close();
preparedStatement.close();;
}
结果
批量操作
PreparedStatement:
1) addBatch()将一组参数添加到PreparedStatement对象内部。
2) executeBatch()将一批参数提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组。
Statement:
1) addBatch(String sql)方法会在批处理缓存中加入一条sql语句。
2) executeBatch()执行批处理缓存中的所有sql语句。
注意:PreparedStatement中使用批量更新时,要先设置好参数后再使用addBatch()方法加入缓存。批量更新中只能使用更改
、删除
或插入
语句
PreparedStatement批量插入代码
@Test
public void preparedStatementBatchTest() throws SQLException {
//插入语句
String sql = "INSERT INTO student (`name`,age) VALUES (?,?)";
//通过insert创建预编译声明
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long startTime = System.currentTimeMillis();
//每一次遍历添加一组参数
for (int i = 0; i < 100; i++) {
preparedStatement.setString(1, UUID.randomUUID().toString());
preparedStatement.setInt(2, (int)(Math.random()*10+1) );
preparedStatement.addBatch();
}
// 批处理 一次发射
preparedStatement.executeBatch();
System.out.println(String.format("批量处理使用了:【%d】ms",System.currentTimeMillis() - startTime));
preparedStatement.close();
}
Mybatis的执行过程
SqlSession(Sql会话)
SqlSession为mybatis对外的大管家,采用门面模式(外观模式)
提供了基础的增删改查的API,也提供了提交、关闭会话以及缓存相关的辅助Api
SqlSession中有两个重要的成员变量
- configuration: 当前会话的配置
- executor:具体处理数据库操作的执行器
创建SqlSession的过程
通过DefaultSqlSessionFactory实例化的SqlSession是线程不安全的。提供了多个重载的openSesson的方法
SqlSession openSession();
//是否自动提交
SqlSession openSession(boolean autoCommit);
//通过数据库连接获取SqlSessonn
SqlSession openSession(Connection connection);
//事务的隔离级别
SqlSession openSession(TransactionIsolationLevel level);
//执行器的类型
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
通过数据库获取SqlSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取当前的环境信息
final Environment environment = configuration.getEnvironment();
//获取事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//事务的创建
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//执行器的创建
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
通过连接获取SqlSession
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;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
其中比较关键的就是Executor executor = configuration.newExecutor(tx, execType);
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//判断参数,如果为null取默认的SimpleExecutor
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//判断二级缓存是否开启,开启时创建CachingExecutor对原有的Executor进行装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
通过SqlSessionManager.startSession()创建的是线程安全的。
//维护了ThreadLocal变量
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
public void startManagedSession() { this.localSqlSession.set(openSession()); }
Executor(执行器)
SqlSession是面向程序的,而Executor是面向数据库的,SqlSession的所有的方法都会调用到自己内部注册的Executor类的query和update方法执行具体的方法,采用的是模板方法的设计模式。执行器的种类有:基础执行器、简单执行器、重用执行器和批处理执行器,此外通过装饰器形式添加了一个缓存执行器。对应功能包括缓存处理、事物处理、重用处理以及批处理,而这些都是会话中多个SQL执行中有共性地方。执行器存在的意义就是去处理这些共性。
Executor的继承类图和简介:
BaseExecutor (基础执行器)
BaseExecutor采用的模板方法的设计模式实现了Executor接口,并处理了一级缓存的相关逻辑(在后续缓存章节讲解)。
下面的四个方法是真正执行Sql语句的方法,BaseExecutor中并没有实现其逻辑,都由其子类实现
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
SimpleExecutor(简单执行器)
SimpleExecutor为最基础的执行器,每一次查询都会创建一个PreparedStatament,再没有配置ExecutortType或者配置的为null
时,默认的都会使用此执行器。
ReuseExecutor (可重用执行器)
ReuseExecutor 为可重用执行器,当执行的相同的sql语句时会重用Statement,实现逻辑也比较简单。
- 内部维护一个HashMap来缓存Statement
- 执行query或者update前,先判断当前的sql语句在map中是否已经存在,如果存在返回缓存的Statement,不存在编译后缓存,key值为sql,value为Statement
具体代码如下:
//用于缓存Statement的HashMap
private final Map<String, Statement> statementMap = new HashMap<>();
//继承自BaseExecutor的相关的执行逻辑的方法
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}
//准备Statement的方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
//判断当前sql是否已经缓存
if (hasStatementFor(sql)) {
//命中时直接返回
stmt = getStatement(sql);
//应用事务的超时时间,如果查询的超时时间为0,或者事务的超时时间小于查询的超时时间时,设置查询的超时时间为事务的超时时间。
applyTransactionTimeout(stmt);
} else {
//准备Statement放入缓存
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
//判断是否命中缓存
private boolean hasStatementFor(String sql) {
try {
return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
} catch (SQLException e) {
return false;
}
}
//获取缓存中的Statement
private Statement getStatement(String s) {
return statementMap.get(s);
}
//放置缓存中的Statement
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}
BaseExecutor中更新查询的超时时间
//应用事务的超时时间,如果查询的超时时间为0,或者事务的超时时间小于查询的超时时间时,设置查询的超时时间为事务的超时时间。
protected void applyTransactionTimeout(Statement statement) throws SQLException {
StatementUtil.applyTransactionTimeout(statement, statement.getQueryTimeout(), transaction.getTimeout());
}
StatementUtil的applyTransactionTimeout方法
public static void applyTransactionTimeout(Statement statement, Integer queryTimeout, Integer transactionTimeout) throws SQLException {
if (transactionTimeout == null){
return;
}
Integer timeToLiveOfQuery = null;
if (queryTimeout == null || queryTimeout == 0) {
timeToLiveOfQuery = transactionTimeout;
} else if (transactionTimeout < queryTimeout) {
timeToLiveOfQuery = transactionTimeout;
}
if (timeToLiveOfQuery != null) {
statement.setQueryTimeout(timeToLiveOfQuery);
}
}
BatchExecutor(批处理执行器)
BatchExecutor可以批量执行非查询操作,当相同和连续的Statement执行时,会公用同一个Statement,但必须要执行SqlSession的flushStatements
才会生效,具体实现的代码如下:
声明的记录缓存的变量
//执行的Statement记录
private final List<Statement> statementList = new ArrayList<>();
//当前执行的语句
private String currentSql;
//处理结果集合
private final List<BatchResult> batchResultList = new ArrayList<>();
//当前的Statement
private MappedStatement currentStatement;
具体的判断逻辑
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和Statement和记录的是否相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
//命中时,获取当前的最后一个Statement
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);//fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); //fix Issues 322
//为命中时,给current和currentStatement赋值
currentSql = sql;
currentStatement = ms;
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
批量提交操作
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);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
//执行结构封装
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
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
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);
}
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
以上三种执行器如何指定
配置文件中配置默认的executor
<settings>
<setting name="defaultExecutorType" value="REUSE"/>
</settings>
创建SqlSesson中使用带有ExecutorType的方法
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
CachingExecutor(二级缓存执行器)
CachingExecutor使用了装饰者模式,对其他三种执行器进行了包装,使其有了二级缓存的功能。其内部的执行sql的方法都调用其内部包装的Executor对象,二级缓存具体如何工作,在后面的缓存章节进行介绍
简单列出CachingExecutor的构造方法和update的实现
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
SqlSession调用Executor的执行过程
执行堆栈
StatementHandler(声明处理器)
未完