該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址、Mybatis-Spring 原始碼分析 GitHub 地址、Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的SQL執行過程
在前面一系列的文件中,我已經分析了 MyBatis 的基礎支援層以及整個的初始化過程,此時 MyBatis 已經處於就緒狀態了,等待使用者發號施令了
那麼接下來我們來看看它執行SQL的整個過程,該過程比較複雜,涉及到二級快取,將返回結果轉換成 Java 物件以及延遲載入等等處理過程,這裡將一步一步地進行分析:
- 《SQL執行過程(一)之Executor》
- 《SQL執行過程(二)之StatementHandler》
- 《SQL執行過程(三)之ResultSetHandler》
- 《SQL執行過程(四)之延遲載入》
MyBatis中SQL執行的整體過程如下圖所示:
在 SqlSession 中,會將執行 SQL 的過程交由Executor
執行器去執行,過程大致如下:
- 通過
DefaultSqlSessionFactory
建立與資料庫互動的SqlSession
“會話”,其內部會建立一個Executor
執行器物件 - 然後
Executor
執行器通過StatementHandler
建立對應的java.sql.Statement
物件,並通過ParameterHandler
設定引數,然後執行資料庫相關操作 - 如果是資料庫更新操作,則可能需要通過
KeyGenerator
先設定自增鍵,然後返回受影響的行數 - 如果是資料庫查詢操作,則需要將資料庫返回的
ResultSet
結果集物件包裝成ResultSetWrapper
,然後通過DefaultResultSetHandler
對結果集進行對映,最後返回 Java 物件
上面還涉及到一級快取、二級快取和延遲載入等其他處理過程
SQL執行過程(二)之StatementHandler
在上一篇文件中,已經詳細地分析了在MyBatis的SQL執行過程中,SqlSession會話將資料庫操作交由Executor執行器去完成,實際上需要通過StatementHandler
建立相應的Statement
物件,並做一些準備工作,然後通過Statement
執行資料庫操作,查詢結果則需要通過ResultSetHandler
對結果集進行對映轉換成Java物件,那麼接下來我們先來看看StatementHandler
到底做哪些操作
StatementHandler介面的實現類如下圖所示:
-
org.apache.ibatis.executor.statement.RoutingStatementHandler
:實現StatementHandler介面,裝飾器模式,根據Statement型別建立對應的StatementHandler物件,所有的方法執行交由該物件執行 -
org.apache.ibatis.executor.statement.BaseStatementHandler
:實現StatementHandler介面,提供骨架方法,指定的幾個抽象方法交由不同的子類去實現 -
org.apache.ibatis.executor.statement.SimpleStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.Statement
進行資料庫操作 -
org.apache.ibatis.executor.statement.PreparedStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.PreparedStatement
進行資料庫操作(預設) -
org.apache.ibatis.executor.statement.CallableStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.CallableStatement
進行資料庫操作,用於儲存過程
我們先回顧一下StatementHandler是在哪裡被建立的,可以在《SQL執行過程(一)之Executor》的SimpleExecutor小節中有講到,建立的是RoutingStatementHandler
物件
StatementHandler
org.apache.ibatis.executor.statement.StatementHandler
:Statement處理器介面,程式碼如下:
public interface StatementHandler {
/**
* 準備操作,可以理解成建立 Statement 物件
*
* @param connection Connection 物件
* @param transactionTimeout 事務超時時間
* @return Statement 物件
*/
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
/**
* 設定 Statement 物件的引數
*
* @param statement Statement 物件
*/
void parameterize(Statement statement) throws SQLException;
/**
* 新增 Statement 物件的批量操作
*
* @param statement Statement 物件
*/
void batch(Statement statement) throws SQLException;
/**
* 執行寫操作
*
* @param statement Statement 物件
* @return 影響的條數
*/
int update(Statement statement) throws SQLException;
/**
* 執行讀操作
*
* @param statement Statement 物件
* @param resultHandler ResultHandler 物件,處理結果
* @param <E> 泛型
* @return 讀取的結果
*/
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
/**
* 執行讀操作,返回 Cursor 物件
*
* @param statement Statement 物件
* @param <E> 泛型
* @return Cursor 物件
*/
<E> Cursor<E> queryCursor(Statement statement) throws SQLException;
/**
* @return BoundSql 物件
*/
BoundSql getBoundSql();
/**
* @return ParameterHandler 物件
*/
ParameterHandler getParameterHandler();
}
每個方法可以根據註釋先理解它的作用,在實現類中的會講到
RoutingStatementHandler
org.apache.ibatis.executor.statement.RoutingStatementHandler
:實現StatementHandler介面,採用裝飾器模式,在初始化的時候根據Statement型別,建立對應的StatementHandler物件,程式碼如下:
public class RoutingStatementHandler implements StatementHandler {
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
// 根據不同的型別,建立對應的 StatementHandler 實現類
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
}
-
在建構函式中初始化
delegate
委託物件,根據MappedStatement
(每個SQL對應的物件)的statementType
型別,建立對應的StatementHandler實現類 -
其餘所有的方法都是直接交由
delegate
去執行的,這裡就不列出來了,就是實現StatementHandler介面的方法
回顧到《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》中的XMLStatementBuilder小節,在parseStatementNode
方法中的第10
步如下:
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
所以說statementType
的預設值為PREPARED
,委託物件也就是PreparedStatementHandler
型別
BaseStatementHandler
org.apache.ibatis.executor.statement.BaseStatementHandler
:實現StatementHandler介面,提供骨架方法,指定的幾個抽象方法交由不同的子類去實現
構造方法
public abstract class BaseStatementHandler implements StatementHandler {
/**
* 全域性配置
*/
protected final Configuration configuration;
/**
* 例項工廠
*/
protected final ObjectFactory objectFactory;
/**
* 型別處理器登錄檔
*/
protected final TypeHandlerRegistry typeHandlerRegistry;
/**
* 執行結果處理器
*/
protected final ResultSetHandler resultSetHandler;
/**
* 引數處理器,預設 DefaultParameterHandler
*/
protected final ParameterHandler parameterHandler;
/**
* 執行器
*/
protected final Executor executor;
/**
* SQL 相關資訊
*/
protected final MappedStatement mappedStatement;
/**
* 分頁條件
*/
protected final RowBounds rowBounds;
/**
* SQL 語句
*/
protected BoundSql boundSql;
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();
// <1> 如果 boundSql 為空,更新資料庫的操作這裡傳入的物件會為 null
if (boundSql == null) { // issue #435, get the key before calculating the statement
// <1.1> 生成 key,定義了 <selectKey /> 且配置了 order="BEFORE",則在 SQL 執行之前執行
generateKeys(parameterObject);
// <1.2> 建立 BoundSql 物件
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
// <2> 建立 ParameterHandler 物件,預設為 DefaultParameterHandler
// PreparedStatementHandler 實現的 parameterize 方法中需要對引數進行預處理,進行引數化時需要用到
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
// <3> 建立 DefaultResultSetHandler 物件
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
}
關於它的屬性可以根據註釋進行理解
-
如果入參中的
boundSql
為null
,則需要進行初始化,可以會看到SimpleExecutor
中執行資料庫的更新操作時,傳入的boundSql
為null
,資料庫的查詢操作才會傳入該物件的值-
呼叫
generateKeys(Object parameter)
方法,根據配置的KeyGenerator
物件,在SQL執行之前執行查詢操作獲取值,設定到入參物件對應屬性中,程式碼如下:protected void generateKeys(Object parameter) { /* * 獲得 KeyGenerator 物件 * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件 * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件 * 否則為 NoKeyGenerator 物件 */ KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); ErrorContext.instance().store(); // 前置處理,建立自增編號到 parameter 中 keyGenerator.processBefore(executor, mappedStatement, null, parameter); ErrorContext.instance().recall(); }
只有配置的
<selectKey />
標籤才有前置處理,這就是為什麼資料庫的更新操作傳入的boundSql
為null
的原因,因為入參中有的屬性值可能需要提前生成一個值(執行配置的SQL語句),KeyGenerator
會在後續講到? -
通過
MappedStatement
物件根據入參獲取BoundSql
物件,在《MyBatis初始化(四)之SQL初始化(下)》中的SqlSource小節中有講到這個方法,如果是動態SQL則需要進行解析,獲取到最終的SQL,替換成?
佔位符
-
-
建立
ParameterHandler
物件,用於對引數進行預處理,預設為DefaultParameterHandler
,這個也在《MyBatis初始化(四)之SQL初始化(下)》中有講過可以看到Configuration的
newParameterHandler
方法:public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // 建立 ParameterHandler 物件 ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 應用外掛 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }
-
建立
ResultSetHandler
,用於返回結果的對映,預設為DefaultResultSetHandler
,這個對映過程非常複雜,會有單獨一篇文件進行分析?可以看到Configuration的
newResultSetHandler
方法:public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { // 建立 DefaultResultSetHandler 物件 ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // 應用外掛 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; }
prepare方法
建立Statement物件,做一些初始化工作,程式碼如下:
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// <1> 建立 Statement 物件
statement = instantiateStatement(connection);
// <2> 設定執行和事務的超時時間
setStatementTimeout(statement, transactionTimeout);
// <3> 設定 fetchSize,為驅動的結果集獲取數量(fetchSize)設定一個建議值
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);
}
}
-
建立 Statement 物件,呼叫
instantiateStatement(Connection connection)
抽象方法,交由不同的子類去實現 -
設定執行和事務的超時時間,呼叫
setStatementTimeout(Statement stmt, Integer transactionTimeout)
方法,如下:protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException { Integer queryTimeout = null; // 獲得 queryTimeout if (mappedStatement.getTimeout() != null) { queryTimeout = mappedStatement.getTimeout(); } else if (configuration.getDefaultStatementTimeout() != null) { queryTimeout = configuration.getDefaultStatementTimeout(); } // 設定執行的超時時間 if (queryTimeout != null) { stmt.setQueryTimeout(queryTimeout); } // 設定事務超時時間 StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout); }
-
設定 fetchSize,為驅動的結果集獲取數量(fetchSize)設定一個建議值(無預設值),呼叫
setFetchSize(Statement stmt)
方法,如下:protected void setFetchSize(Statement stmt) throws SQLException { // 獲得 fetchSize 配置 Integer fetchSize = mappedStatement.getFetchSize(); if (fetchSize != null) { stmt.setFetchSize(fetchSize); return; } // 獲得 fetchSize 的預設配置 Integer defaultFetchSize = configuration.getDefaultFetchSize(); if (defaultFetchSize != null) { stmt.setFetchSize(defaultFetchSize); } }
-
發生任何異常都會關閉 Statement 物件
SimpleStatementHandler
org.apache.ibatis.executor.statement.SimpleStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.Statement
進行資料庫操作,部分程式碼如下:
public class SimpleStatementHandler extends BaseStatementHandler {
public SimpleStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
@Override
public void batch(Statement statement) throws SQLException {
String sql = boundSql.getSql();
// 新增到批處理
statement.addBatch(sql);
}
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
// <1> 執行查詢
statement.execute(sql);
// <2> 處理返回結果
return resultSetHandler.handleResultSets(statement);
}
@Override
public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleCursorResultSets(statement);
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
// 建立Statement物件
return connection.createStatement();
} else {
return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
@Override
public void parameterize(Statement statement) {
// N/A
}
}
-
上面的方法都很簡單,都是直接通過
Statement
物件執行資料庫操作 -
在查詢方法中,需要通過
resultSetHandler
對結果集進行對映,返回對應的Java物件
update方法
執行資料庫更新操作,方法如下:
@Override
public int update(Statement statement) throws SQLException {
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
/*
* 獲得 KeyGenerator 物件
* 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
* 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
* 否則為 NoKeyGenerator 物件
*/
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
int rows;
if (keyGenerator instanceof Jdbc3KeyGenerator) { // 如果是 Jdbc3KeyGenerator 型別
// <1.1> 執行寫操作,設定返回自增鍵,可通過 getGeneratedKeys() 方法獲取
statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
// <1.2> 獲得更新數量
rows = statement.getUpdateCount();
// <1.3> 執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else if (keyGenerator instanceof SelectKeyGenerator) { // 如果是 SelectKeyGenerator 型別
// <2.1> 執行寫操作
statement.execute(sql);
// <2.2> 獲得更新數量
rows = statement.getUpdateCount();
// <2.3>執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else {
// <3.1> 執行寫操作
statement.execute(sql);
// <3.2> 獲得更新數量
rows = statement.getUpdateCount();
}
return rows;
}
為什麼資料庫更新操作的方法需要做這麼多處理,其實目的就一個,支援使用者配置的自增鍵,設定到入參中
在BaseStatementHandler
的構造方法已經有過KeyGenerator
的前置處理了,那裡是在SQL執行之前,執行查詢操作獲取值,設定到入參物件對應屬性中
而這裡需要做的就是在SQL執行的後置處理了,在SQL執行之後,執行查詢操作獲取值或者設定需要返回哪些自增鍵,設定到入參物件對應屬性中
-
如果
KeyGenerator
是Jdbc3KeyGenerator
型別,也就是配置useGeneratedKeys="true"
- 執行寫操作,設定需要返回自增鍵,可通過
getGeneratedKeys()
方法獲取 - 獲得受影響的行數
- 執行後置處理,呼叫其
processAfter
方法,也就是將我們配置的自增鍵設定到入參物件中
- 執行寫操作,設定需要返回自增鍵,可通過
-
如果
KeyGenerator
是SelectKeyGenerator
型別,也就是新增了<selectKey />
標籤- 執行寫操作
- 獲得受影響的行數
- 執行後置處理,呼叫其
processAfter
方法,也就是執行查詢操作獲取值,設定到入參物件對應屬性中
-
如果沒有配置
KeyGenerator
- 執行寫操作
- 獲得受影響的行數
PreparedStatementHandler
org.apache.ibatis.executor.statement.PreparedStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.PreparedStatement
進行資料庫操作(預設),部分程式碼如下:
public class PreparedStatementHandler extends BaseStatementHandler {
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
@Override
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 新增到批處理
ps.addBatch();
}
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行
ps.execute();
// 結果處理器並返回結果
return resultSetHandler.handleResultSets(ps);
}
@Override
public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行
ps.execute();
// 結果處理器並返回 Cursor 結果
return resultSetHandler.handleCursorResultSets(ps);
}
}
-
上面的方法都很簡單,都是直接通過
PreparedStatement
物件執行資料庫操作 -
在查詢方法中,需要通過
resultSetHandler
對結果集進行對映,返回對應的Java物件
instantiateStatement方法
建立一個PreparedStatement
物件,方法如下:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
/*
* 獲得 KeyGenerator 物件
* 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
* 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
* 否則為 NoKeyGenerator 物件
*/
// <1> 處理 Jdbc3KeyGenerator 的情況
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
// <1.1> 獲得 keyColumn 配置
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
// <1.2 >建立 PreparedStatement 物件,並返回自增鍵,並可通過 getGeneratedKeys() 方法獲取
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
// <1.3> 建立 PreparedStatement 物件,並返回我們配置的 column 列名自增鍵,並可通過 getGeneratedKeys() 方法獲取
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
// <2> 建立 PrepareStatement 物件
return connection.prepareStatement(sql);
} else {
// <3> 建立 PrepareStatement 物件,指定 ResultSetType
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
-
處理
Jdbc3KeyGenerator
的情況,也就是配置了useGeneratedKeys="true"
- 獲得
keyColumn
配置,哪些自增鍵需要返回 - 如果keyColumn為null,返回
PreparedStatement
物件,設定RETURN_GENERATED_KEYS
,表示所有自增列都返回,可通過getGeneratedKeys()
方法獲取 - 如果keyColumn不為null,返回
PreparedStatement
物件,設定需要返回的自增列為keyColumn,可通過getGeneratedKeys()
方法獲取
- 獲得
-
沒有配置Jdbc3KeyGenerator物件,建立
PreparedStatement
物件返回,預設情況 -
沒有配置Jdbc3KeyGenerator物件,但是指定了ResultSetType,則返回
PreparedStatement
物件,指定ResultSetType
parameterize方法
設定PreparedStatement的佔位符引數,方法如下:
@Override
public void parameterize(Statement statement) throws SQLException {
// 通過 DefaultParameterHandler 設定 PreparedStatement 的佔位符引數
parameterHandler.setParameters((PreparedStatement) statement);
}
update方法
執行資料庫更新操作,方法如下:
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行
ps.execute();
// 獲得更新數量
int rows = ps.getUpdateCount();
// 入參物件
Object parameterObject = boundSql.getParameterObject();
/*
* 獲得 KeyGenerator 物件
* 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
* 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
* 否則為 NoKeyGenerator 物件
*/
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
// 執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
- 執行資料庫更新操作
- 獲得受影響行數
- 根據配置的
KeyGenerator
物件,執行後置處理,執行查詢操作獲取值,或者獲取返回的自增鍵,設定到入參物件對應屬性中
CallableStatementHandler
org.apache.ibatis.executor.statement.CallableStatementHandler
:繼承BaseStatementHandler抽象類,建立java.sql.CallableStatement
進行資料庫操作,用於儲存過程
在執行完資料庫的操作後需要呼叫DefaultResultSetHandler
的handleOutputParameters
方法,處理需要作為出參的引數,這裡就不做過得的講述了?
KeyGenerator
在上面已經講到在執行資料庫更新操作時,需要通過KeyGenerator來進行前置處理或者後置處理,我一般用於自增主鍵
先來看看它的實現類,如下圖所示:
-
org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator
:實現KeyGenerator介面,只有後置處理的實現,獲取返回的自增鍵,設定到入參的屬性中,配置了useGeneratedKeys="true"
則會建立該物件 -
org.apache.ibatis.executor.keygen.SelectKeyGenerator
:實現KeyGenerator介面,執行資料庫查詢操作,獲取到對應的返回結果設定到入參的屬性中,新增了<selectKey />
標籤則會建立該物件,前後置處理可以配置 -
org.apache.ibatis.executor.keygen.NoKeyGenerator
:實現KeyGenerator介面,空實現,一個單例,沒有配置上面的兩種方式,則預設為該物件
KeyGenerator介面,程式碼如下:
public interface KeyGenerator {
/**
* 在 SQL 執行後設定自增鍵到入參中
*
* @param executor 執行器
* @param ms MappedStatement 物件
* @param stmt Statement物件
* @param parameter 入參物件
*/
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
/**
* 在 SQL 執行前設定自增鍵到入參中
*
* @param executor 執行器
* @param ms MappedStatement 物件
* @param stmt Statement物件
* @param parameter 入參物件
*/
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
Jdbc3KeyGenerator
org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator
:實現KeyGenerator介面,只有後置處理的實現,獲取返回的自增鍵,設定到入參的屬性中,配置了useGeneratedKeys="true"
則會建立該物件,在《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》的XMLStatementBuilder小節的parseStatementNode
方法中的第8
步看到
實現方法:
public class Jdbc3KeyGenerator implements KeyGenerator {
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// do nothing
}
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// 批處理多個自增鍵
processBatch(ms, stmt, parameter);
}
}
可以看到前置處理的實現方法為空,後置處理的實現方法呼叫processBatch
方法
processBatch方法
processBatch(MappedStatement ms, Statement stmt, Object parameter)
方法,從結果集中獲自增鍵設定到入參物件的屬性中,程式碼如下:
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
// 獲取 keyProperty 配置
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
// 獲取 Statement 執行後自增鍵對應的 ResultSet 物件
try (ResultSet rs = stmt.getGeneratedKeys()) {
// 獲取 ResultSet 的 ResultSetMetaData 後設資料物件
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) { // 自增鍵與 keyProperty 數量不一致則跳過
// Error?
} else {
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}
- 獲取
keyProperty
配置,也就是需要將自增鍵設定到入參物件的屬性名稱 - 通過
Statement
的getGeneratedKeys()
方法獲取到自增鍵 - 如果自增鍵與屬性個數不相同則跳過,不進行處理了
- 否則呼叫
assignKeys
方法,分配自增鍵給對應的屬性
assignKeys方法
/**
* 關於 ParamMap,可以看到 {@link org.apache.ibatis.reflection.ParamNameResolver#getNamedParams} 這個方法
* 根據入參獲取獲取引數名稱與引數值的對映關係
* 如果為多個入參或者一個入參並且新增了 @Param 註解,則會返回 ParamMap 物件,key為引數名稱,value為引數值
*
* 需要使用到獲取自增鍵時,我們一般入參都是一個實體類,則進入的是下面第3種情況
*/
@SuppressWarnings("unchecked")
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter) throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) { // <1>
// Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) { // <2>
// Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
} else { // <3>
// Single param without @Param
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}
- 因為我的入參通常是一個實體類的時候,配置自增鍵的生成,這裡我們直接看第三種情況,其他兩種可參考我的註釋進行閱讀
assignKeysToParam方法
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
String[] keyProperties, Object parameter) throws SQLException {
// 將入參物件 parameter 轉換成集合,因為批處理時可能傳入多個入參物件
Collection<?> params = collectionize(parameter);
if (params.isEmpty()) {
return;
}
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
// 每一個 keyProperty 都建立一個 KeyAssigner 物件,設定 column 的位置
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
}
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
}
Object param = iterator.next();
// 往每個入參物件中設定配置的 keyProperty 為對應的自增鍵
assignerList.forEach(x -> x.assign(rs, param));
}
}
- 將入參物件
parameter
轉換成集合,因為批處理時可能傳入多個入參物件 - 每一個
keyProperty
都建立一個KeyAssigner
物件,設定自增鍵的位置,通過該物件往入參中設定該屬性值 - 遍歷入參
params
集合,每個入參物件都遍歷assignerList
集合,通過KeyAssigner
往入參中設定屬性值
KeyAssigner
定義在Jdbc3KeyGenerator的一個內部類,單個自增鍵的分配者,將該自增鍵設定到入參物件的屬性中,構造方法如下:
private class KeyAssigner {
/**
* 全域性配置物件
*/
private final Configuration configuration;
/**
* Statement 執行後返回的 後設資料物件
*/
private final ResultSetMetaData rsmd;
/**
* 型別處理器註冊中心
*/
private final TypeHandlerRegistry typeHandlerRegistry;
/**
* 屬性對應列所在的位置
*/
private final int columnPosition;
/**
* 引數名稱,新增了 @Param 註解時才有
*/
private final String paramName;
/**
* Java 屬性名稱
*/
private final String propertyName;
/**
* 型別處理器
*/
private TypeHandler<?> typeHandler;
protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
String propertyName) {
super();
this.configuration = configuration;
this.rsmd = rsmd;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.columnPosition = columnPosition;
this.paramName = paramName;
this.propertyName = propertyName;
}
}
- 定義了結果後設資料
ResultSetMetaData
物件、自增鍵所在位置、屬性名稱、型別處理器
assign方法
assign(ResultSet rs, Object param)
方法,將當前自增鍵設定到param
入參的屬性中,程式碼如下:
protected void assign(ResultSet rs, Object param) {
if (paramName != null) {
// If paramName is set, param is ParamMap
param = ((ParamMap<?>) param).get(paramName);
}
MetaObject metaParam = configuration.newMetaObject(param);
try {
if (typeHandler == null) {
if (metaParam.hasSetter(propertyName)) {
Class<?> propertyType = metaParam.getSetterType(propertyName);
// 根據 Java Type 和 Jdbc Type 獲取對應的型別處理器
typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition)));
} else {
throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
+ metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
if (typeHandler == null) {
// Error?
} else {
// 將 Jdbc Type 轉換成 Java Type
Object value = typeHandler.getResult(rs, columnPosition);
// 將該屬性值設定到 入參物件中
metaParam.setValue(propertyName, value);
}
} catch (SQLException e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
e);
}
}
- 從結果集中根據位置獲取到該自增鍵,然後設定到入參物件的屬性中
SelectKeyGenerator
org.apache.ibatis.executor.keygen.SelectKeyGenerator
:實現KeyGenerator介面,執行資料庫查詢操作,獲取到對應的返回結果設定到入參的屬性中,新增了<selectKey />
標籤則會建立該物件,前後置處理可以配置,在《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》的XMLStatementBuilder小節的parseStatementNode
方法中的第7
步看到
實現方法:
public class SelectKeyGenerator implements KeyGenerator {
/**
* <selectKey /> 解析成 MappedStatement 物件的 id 的字尾
* 例如 <selectKey /> 所在的 MappedStatement 的 id 為 namespace.test
* 那麼它的 id 就是 namespace.test!selectKey
*/
public static final String SELECT_KEY_SUFFIX = "!selectKey";
/**
* 是否在 SQL 執行後執行
*/
private final boolean executeBefore;
/**
* <selectKey /> 解析成 MappedStatement 物件
*/
private final MappedStatement keyStatement;
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) { // 如果是在 SQL 執行之前進行生成 key
processGeneratedKeys(executor, ms, parameter);
}
}
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!executeBefore) { // 如果是在 SQL 執行之後進行生成 key
processGeneratedKeys(executor, ms, parameter);
}
}
}
-
可以看到
<selectKey />
標籤還會生成一個MappedStatement
物件,用於執行查詢語句 -
executeBefore
屬性表示,是否為前置處理,所以<selectKey />
要麼就是前置處理,要麼就是後置處理,都是呼叫processGeneratedKeys
方法
processGeneratedKeys方法
執行資料庫的查詢操作,生成“主鍵”,設定到入參物件的屬性中,程式碼如下:
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
// <1> 獲取 keyProperty 配置
String[] keyProperties = keyStatement.getKeyProperties();
final Configuration configuration = ms.getConfiguration();
// <2> 建立入參 MetaObject 物件
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (keyProperties != null) {
// Do not close keyExecutor.
// The transaction will be closed by parent executor.
// <3> 建立一個 SimpleExecutor 執行器
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
// <4> 執行資料庫查詢
List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
if (values.size() == 0) {
throw new ExecutorException("SelectKey returned no data.");
} else if (values.size() > 1) {
throw new ExecutorException("SelectKey returned more than one value.");
} else {
// <5> 為資料庫查詢的到資料建立 MetaObject 物件
MetaObject metaResult = configuration.newMetaObject(values.get(0));
if (keyProperties.length == 1) { // <6.1> 單個屬性
if (metaResult.hasGetter(keyProperties[0])) {
// 往入參中設定該屬性為從資料庫查詢到的資料
setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
} else {
// no getter for the property - maybe just a single value object
// so try that
setValue(metaParam, keyProperties[0], values.get(0));
}
} else { //<6.2> 多個屬性
handleMultipleProperties(keyProperties, metaParam, metaResult);
}
}
}
}
} catch (ExecutorException e) {
throw e;
} catch (Exception e) {
throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
}
}
private void setValue(MetaObject metaParam, String property, Object value) {
if (metaParam.hasSetter(property)) {
metaParam.setValue(property, value);
} else {
throw new ExecutorException("省略...");
}
}
- 獲取
keyProperty
配置,也就是需要設定值到入參中的屬性名稱 - 建立入參的
MetaObject
物件metaParam
,便於操作 - 建立一個
SimpleExecutor
執行器 - 使用該執行器執行資料庫查詢操作
- 為資料庫查詢獲取到的結果建立
MetaObject
物件metaResult
- 將查詢結果設定到入參物件中
- 如果
keyProperty
配置的僅僅是一個屬性,則從metaResult
中獲取查詢結果設定到metaParam
入參物件的該屬性中 - 如果多個屬性需要賦值,則呼叫
handleMultipleProperties
方法將查詢結果設定到入參物件的多個屬性中
- 如果
handleMultipleProperties方法
private void handleMultipleProperties(String[] keyProperties, MetaObject metaParam, MetaObject metaResult) {
// 獲取 keyColumn 配置
String[] keyColumns = keyStatement.getKeyColumns();
if (keyColumns == null || keyColumns.length == 0) { // 沒有配置列名則直接去屬性名
// no key columns specified, just use the property names
for (String keyProperty : keyProperties) {
// 往入參中設定該屬性為從資料庫查詢到的資料,從查詢到的結果中取屬性名
setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
}
} else {
if (keyColumns.length != keyProperties.length) { // 列名和屬性名的個數不一致則丟擲異常
throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
}
for (int i = 0; i < keyProperties.length; i++) {
// 往入參中設定該屬性為從資料庫查詢到的資料,從查詢到的結果中取列名
setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
}
}
}
- 沒有配置列名,則根據
keyProperty
屬性名稱從metaResult
查詢結果中獲取結果,一個一個設定到metaParam
入參物件的該屬性中 - 沒有配置列名,則根據
keyColumn
列名從metaResult
查詢結果中獲取結果,一個一個設定到metaParam
入參物件的該屬性中
NoKeyGenerator
org.apache.ibatis.executor.keygen.NoKeyGenerator
:實現KeyGenerator介面,空實現,一個單例,沒有配置上面的兩種方式,則預設為該物件,程式碼如下:
public class NoKeyGenerator implements KeyGenerator {
/**
* A shared instance.
* @since 3.4.3
*/
public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// Do Nothing
}
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// Do Nothing
}
}
總結
本文分析了MyBatis在執行SQL的過程中,SimpleExecutor
(預設型別)執行器需要通過PrepareStatementHandler
(預設)來執行資料庫的操作,建立PrepareStatement
(預設)物件來完成資料操作
如果你配置了useGeneratedKeys="true"
,則需要在執行完資料庫更新操作後,通過Jdbc3KeyGenerator
設定自增鍵到入參物件中(後置處理)
如果你新增了<selectKey />
標籤,則需要通過SelectKeyGenerator
執行資料庫查詢操作獲取到結果,設定到入參物件中(前置處理、後置處理)
如果是查詢操作則需要通過ResultSetHandler
對結果集進行對映轉換成Java物件,這就是我們下一篇文件需要分析的內容?
參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》