深入認識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(聲明處理器)
未完