主頁 > 軟體設計 > 一萬一千字!結合代碼超詳細講解SQL執行流程(二)!干貨到底!建議收藏!

一萬一千字!結合代碼超詳細講解SQL執行流程(二)!干貨到底!建議收藏!

2021-01-12 12:18:55 軟體設計

上文我們已經學習到查詢SQL陳述句的執行程序中如何獲取 BoundSql!接下來繼續從查詢SQL陳述句的執行程序中如何創建 StatementHandler!喜歡的朋友們可以來個一鍵三連哦~

目錄

  • 查詢SQL陳述句的執行程序
    • 2.3 創建 StatementHandler
    • 2.4 設定運?時引數到 SQL 中
    • 2.5 #{}占位符的決議與引數的設定程序梳理
    • 2.6 處理查詢結果
      • 1.創建物體類物件
      • 2.結果集映射
      • 3.關聯查詢與延遲加載
      • 4.存盤映射結果

查詢SQL陳述句的執行程序

2.3 創建 StatementHandler

在 MyBatis 的原始碼中,StatementHandler 是一個非常核心介面,之所以說它核心,是因
為從代碼分層的角度來說,StatementHandler 是 MyBatis 原始碼的邊界,再往下層就是 JDBC 層面的介面了,StatementHandler 需要和 JDBC 層面的介面打交道,它要做的事情有很多,在執行 SQL 之前,StatementHandler 需要創建合適的 Statement 物件,然后填充引數值到
Statement 物件中,最后通過 Statement 物件執行 SQL,這還不算完,待 SQL 執行完畢,還要去處理查詢結果等,這些程序看似簡單,但實作起來卻很復雜,好在,這些程序對應的邏輯并不需要我們親自實作,好了,其他的就不多說了,下面我們來看一下 StatementHandler 的繼承體系,
在這里插入圖片描述

上圖中,最下層的三種 StatementHandler 實作類與三種不同的 Statement 進行互動,這
個不難看出來,但 RoutingStatementHandler 則是一個奇怪的存在,因為 JDBC 中并不存在
RoutingStatement,那它有什么用呢?接下來,我們到代碼中尋找答案,

// -☆- Configuration
public StatementHandler newStatementHandler(Executor executor, 
MappedStatement mappedStatement,Object parameterObject, 
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 創建具有路由功能的 StatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(
executor, mappedStatement, parameterObject, rowBounds, 
resultHandler, boundSql);
// 應用插件到 StatementHandler 上
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler; }

如上,newStatementHandler 方法在創建 StatementHandler 之后,還會應用插件到
StatementHandler 上,關于 MyBatis 的插件機制,后面獨立成章進行講解,這里就不分析了,下面分析 RoutingStatementHandler 的代碼,

public class RoutingStatementHandler implements StatementHandler {
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms, 
Object parameter, RowBounds rowBounds, ResultHandler resultHandler, 
BoundSql boundSql) {
// 根據 StatementType 創建不同的 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("……");
 }
 }
// 其他方法邏輯均由別的 StatementHandler 代理完成,就不貼代碼了
}

RoutingStatementHandler 的構造方法會根據 MappedStatement 中的 statementType 變數創建不同的 StatementHandler 實作類,默認情況下,statementType 值為 PREPARED,關于StatementHandler 創建的程序就先分析到這,StatementHandler 創建完成了,后續要做到事情是創建 Statement,以及將運行時引數和 Statement 進行系結,

2.4 設定運?時引數到 SQL 中

JDBC 提供了三種 Statement 介面,分別是 Statement 、 PreparedStatement 和
CallableStatement,他們的關系如下:
上面三個介面的層級分明,其中 Statement 介面提供了執行 SQL,獲取執行結果等基本
功能,PreparedStatement 在此基礎上,對 IN 型別的引數提供了支持,使得我們可以使用運
行時引數替換 SQL 中的問號?占位符,而不用手動拼接 SQL,CallableStatement 則是在
PreparedStatement 基礎上,對 OUT 型別的引數提供了支持,該種型別的引數用于保存存盤
程序輸出的結果,本節將分析 PreparedStatement 的創建,以及設定運行時引數到 SQL 中的程序,其他兩種 Statement 的處理程序,大家請自行分析,Statement 的創建入口是在
SimpleExecutor 的 prepareStatement 方法中,下面從這個方法開始進行分析,

// -☆- SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log
statementLog) throws SQLException {
Statement stmt;
// 獲取資料庫連接
Connection connection = getConnection(statementLog);
// 創建 Statement,
stmt = handler.prepare(connection, transaction.getTimeout());
// 為 Statement 設定 IN 引數
handler.parameterize(stmt);
return stmt; }

上面代碼的邏輯比較簡單,總共包含三個步驟,如下:

  1. 獲取資料庫連接
  2. 創建 Statement
  3. 為 Statement 設定 IN 引數

上面三個步驟看起來并不難實作,實際上如果大家愿意寫的話,也能寫出來,不過
MyBatis 對這三個步驟進行了一些拓展,實作上也相對復雜一些,以獲取資料庫連接為例,
MyBatis 并未沒有在 getConnection 方法中直接呼叫 JDBC DriverManager 的 getConnection 方法獲取獲取連接,而是通過資料源獲取連接,MyBatis 提供了兩種基于 JDBC 介面的資料源,分別為 PooledDataSource 和 UnpooledDataSource,創建或獲取資料庫連接的操作最終是由這兩個資料源執行,本節不會分析以上兩種資料源的原始碼,相關分析會在下一章中展開,
接下來,我將分析 PreparedStatement 的創建,以及 IN 引數設定的程序,按照順序,先
來分析 PreparedStatement 的創建程序,如下:

// -☆- PreparedStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) 
throws SQLException {
Statement statement = null;
try {
// 創建 Statement
statement = instantiateStatement(connection);
// 設定超時和 FetchSize
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
 } catch (SQLException e) {
closeStatement(statement);
throw e;
 } catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("……");
 } }
protected Statement instantiateStatement(Connection connection) 
throws SQLException {
String sql = boundSql.getSql();
// 根據條件呼叫不同的 prepareStatement 方法創建 PreparedStatement
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() != null) {
return connection.prepareStatement(sql, 
mappedStatement.getResultSetType().getValue(), 
ResultSet.CONCUR_READ_ONLY);
 } else {
return connection.prepareStatement(sql);
 } }

PreparedStatement 的創建程序沒什么復雜的地方,就不多說了,下面分析運行時引數
是如何被設定到 SQL 中的程序,

// -☆- PreparedStatementHandler
public void parameterize(Statement statement) throws SQLException {
// 通過引數處理器 ParameterHandler 設定運行時引數到 PreparedStatement 中
parameterHandler.setParameters((PreparedStatement) statement);
}
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
public void setParameters(PreparedStatement ps) {
// 從 BoundSql 中獲取 ParameterMapping 串列,每個 ParameterMapping 
// 與原始 SQL 中的 #{xxx} 占位符一一對應
List<ParameterMapping> parameterMappings =
boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping=parameterMappings.get(i);
// 檢測引數型別,排除掉 mode 為 OUT 型別的 parameterMapping
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 獲取屬性名
String propertyName = parameterMapping.getProperty();
// 檢測 BoundSql 的 additionalParameters 是否包含 propertyName
if (boundSql.hasAdditionalParameter(propertyName)) {
value=boundSql.getAdditionalParameter(propertyName);
 } else if (parameterObject == null) {
value = null;
// 檢測運行時引數是否有相應的型別決議器
 } else if (typeHandlerRegistry.hasTypeHandler(
parameterObject.getClass())) {
// 若運行時引數的型別有相應的型別處理器 TypeHandler,則將
// parameterObject 設為當前屬性的值,
value = parameterObject;
 } else {
// 為用戶傳入的引數 parameterObject 創建元資訊物件
MetaObject metaObject =
configuration.newMetaObject(parameterObject);
// 從用戶傳入的引數中獲取 propertyName 對應的值
value = metaObject.getValue(propertyName);
 }
// ---------------------分割線---------------------
TypeHandler typeHandler =
parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
// 此處 jdbcType = JdbcType.OTHER
jdbcType = configuration.getJdbcTypeForNull();
 }
try {
// 由型別處理器 typeHandler 向 ParameterHandler 設定引數
typeHandler.setParameter(ps, i + 1, value, jdbcType);
 } catch (TypeException e) {
throw new TypeException(...);
 } catch (SQLException e) {
throw new TypeException(...);
 }
 }
 }
 }
 } }

如上代碼,分割線以上的大段代碼用于獲取#{xxx}占位符屬性所對應的運行時引數,分
割線以下的代碼則是獲取#{xxx}占位符屬性對應的 TypeHandler,并在最后通過 TypeHandler將運行時引數值設定到 PreparedStatement 中,

2.5 #{}占位符的決議與引數的設定程序梳理

前面兩節的內容比較多,本節將對前兩節的部分內容進行梳理,以便大家能夠更好理解
這兩節內容之間的聯系,假設我們有這樣一條 SQL 陳述句:
SELECT * FROM author WHERE name = #{name} AND age = #{age}
這個 SQL 陳述句中包含兩個#{}占位符,在運行時這兩個占位符會被決議成兩個
ParameterMapping 物件,如下:

ParameterMapping{property='name', mode=IN, 
javaType=class java.lang.String, jdbcType=null, ...}

ParameterMapping{property='age', mode=IN, 
javaType=class java.lang.Integer, jdbcType=null, ...}

SELECT * FROM Author WHERE name = ? AND age = ?
這里假設下面這個方法與上面的 SQL 對應:

Author findByNameAndAge(@Param("name")String name, @Param("age")Integer
age)

該方法的引數串列會被 ParamNameResolver 決議成一個 map,如下:

{ 0: "name", 1: "age"
}

假設該方法在運行時有如下的呼叫:

findByNameAndAge("tianxiaobo", 20) 

此時,需要再次借助 ParamNameResolver 的力量,這次我們將引數名和運行時的引數
值系結起來,得到如下的映射關系,

{
"name": "tianxiaobo",
"age": 20,
"param1": "tianxiaobo",
"param2": 20
}

下一步,我們要將運行時引數設定到 SQL 中,由于原 SQL 經過決議后,占位符資訊已
經被擦除掉了,我們無法直接將運行時引數 SQL 中,不過好在,這些占位符資訊被記錄在
了 ParameterMapping 中了,MyBatis 會將 ParameterMapping 會按照#{}占位符的決議順序存入到 List 中,這樣我們通過 ParameterMapping 在串列中的位置確定它與 SQL 中的哪一個個?占位符相關聯,同時通過 ParameterMapping 中的 property 欄位,我們可以到“引數名與引數值”映射表中查找具體的引數值,這樣,我們就可以將引數值準確的設定到 SQL 中了,此時SQL 如下:

SELECT * FROM Author WHERE name = "tianxiaobo" AND age = 20

整個流程如下圖所示,
在這里插入圖片描述
當運行時引數被設定到 SQL 中后,下一步要做的事情是執行 SQL,然后處理 SQL 執行
結果,對于更新操作,資料庫一般回傳一個 int 行數值,表示受影響行數,這個處理起來比
較簡單,但對于查詢操作,回傳的結果型別多變,處理方式也很復雜,接下來,我們就來看
看 MyBatis 是如何處理查詢結果的,

2.6 處理查詢結果

MyBatis 可以將查詢結果,即結果集 ResultSet 自動映射成物體類物件,這樣使用者就無
需再手動操作結果集,并將資料填充到物體類物件中,這可大大降低開發的作業量,提高工
作效率,在 MyBatis 中,結果集的處理作業由結果集處理器 ResultSetHandler 執行,
ResultSetHandler 是一個介面,它只有一個實作類 DefaultResultSetHandler,結果集的處理入口方法是 handleResultSets,下面來看一下該方法的實作,

public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 獲取第一個結果集

ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 處理結果集
handleResultSet(rsw, resultMap, multipleResults, null);
// 獲取下一個結果集
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
 }
// 以下邏輯均與多結果集有關,就不分析了,代碼省略
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {...}
return collapseSingleResultList(multipleResults);
}
private ResultSetWrapper getFirstResultSet(Statement stmt) 
throws SQLException {
// 獲取結果集
ResultSet rs = stmt.getResultSet();
while (rs == null) {
/*
* 移動 ResultSet 指標到下一個上,有些資料庫驅動可能需要使用者
* 先呼叫 getMoreResults 方法,然后才能呼叫 getResultSet 方法
* 獲取到第一個 ResultSet
*/
if (stmt.getMoreResults()) {
rs = stmt.getResultSet();
 } else {
if (stmt.getUpdateCount() == -1) {
break;
 }
 }
 }
/*
* 這里并不直接回傳 ResultSet,而是將其封裝到 ResultSetWrapper 中,
* ResultSetWrapper 中包含了 ResultSet 一些元資訊,比如列名稱、
* 每列對應的 JdbcType、以及每列對應的 Java 類名(class name,譬如
* java.lang.String)等,
*/
return rs != null ? new ResultSetWrapper(rs, configuration) : null; }

如上,該方法首先從 Statement 中獲取第一個結果集,然后呼叫 handleResultSet 方法對
該結果集進行處理,一般情況下,如果我們不呼叫存盤程序,不會涉及到多結果集的問題,
由于存盤程序并不是很常用,所以關于多結果集的處理邏輯我就不分析了,下面,我們把目
光聚焦在單結果集的處理邏輯上,

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, 
List<Object> multipleResults, ResultMapping parentMapping) 
throws SQLException {
try {
if (parentMapping != null) {
// 多結果集相關邏輯,不分析了
handleRowValues(rsw, resultMap, 
null, RowBounds.DEFAULT, parentMapping);
 } else {
/*
* 檢測 resultHandler 是否為空,ResultHandler 是一個介面,使用者可
* 實作該介面,這樣我們可以通過 ResultHandler 自定義接收查詢結果的
* 動作,比如我們可將結果存盤到 List、Map 亦或是 Set,甚至丟棄,
* 這完全取決于大家的實作邏輯,
*/
if (resultHandler == null) {
// 創建默認的結果處理器
DefaultResultHandler defaultResultHandler =
new DefaultResultHandler(objectFactory);
// 處理結果集的行資料
handleRowValues(rsw, resultMap, 
defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
 } else {
// 處理結果集的行資料
handleRowValues(rsw,resultMap,resultHandler,rowBounds,null);
 }
 }
 } finally {
closeResultSet(rsw.getResultSet());
 } }

在上面代碼中,出鏡率最高的 handleRowValues 方法,該方法用于處理結果集中的數
據,下面來看一下這個方法的邏輯,

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, 
ResultHandler<?> resultHandler,RowBounds rowBounds, 
ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
// 處理嵌套映射,關于嵌套映射本文就不分析了
handleRowValuesForNestedResultMap(rsw, 
resultMap, resultHandler, rowBounds, parentMapping);
 } else {
// 處理簡單映射
handleRowValuesForSimpleResultMap(rsw, 
resultMap, resultHandler, rowBounds, parentMapping);
 } }

handleRowValues 方法中針對兩種映射方式進行了處理,一種是嵌套映射,另一種是簡
單映射,本文所說的嵌套查詢是指中嵌套了一個,關于此種映射的
處理方式本節就不進行分析了,下面我將詳細分析簡單映射的處理邏輯,如下:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, 
ResultMap resultMap, ResultHandler<?> resultHandler,RowBounds rowBounds, 
ResultMapping parentMapping) throws SQLException {
DefaultResultContext<Object> resultContext =
new DefaultResultContext<Object>();
// 根據 RowBounds 定位到指定行記錄
skipRows(rsw.getResultSet(), rowBounds);
// 檢測是否還有更多行的資料需要處理
while (shouldProcessMoreRows(resultContext, rowBounds) &&
rsw.getResultSet().next()) {
// 獲取經過鑒別器處理后的 ResultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(
rsw.getResultSet(), resultMap, null);
// 從 resultSet 中獲取結果
Object rowValue = getRowValue(rsw, discriminatedResultMap);
// 存盤結果
storeObject(resultHandler, resultContext, 
rowValue, parentMapping, rsw.getResultSet());
 } }

上面方法的邏輯較多,這里簡單總結一下,如下:

  1. 根據 RowBounds 定位到指定行記錄
  2. 回圈處理多行資料
  3. 使用鑒別器處理 ResultMap
  4. 映射 ResultSet,得到映射結果 rowValue
  5. 存盤結果

在如上幾個步驟中,鑒別器相關的邏輯就不分析了,不是很常用,第 2 步的檢測邏輯
比較簡單,也忽略了,下面分析第一個步驟對應的代碼邏輯,如下:

private void skipRows(ResultSet rs, RowBounds rowBounds) 
throws SQLException {
// 檢測 rs 的型別,不同的型別行資料定位方式是不同的
if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
// 直接定位到 rowBounds.getOffset() 位置處
rs.absolute(rowBounds.getOffset());
 }
 } else {
for (int i = 0; i < rowBounds.getOffset(); i++) {
/*
* 通過多次呼叫 rs.next() 方法實作行資料定位,
* 當 Offset 數值很大時,這種效率很低下
*/
rs.next();
 }
 } }

MyBatis 默認提供了 RowBounds 用于分頁,從上面的代碼中可以看出,這并非是一個高
效的分頁方式,除了使用 RowBounds,還可以使用一些第三方分頁插件進行分頁,關于第三方的分頁插件,大家請自行查閱資料,這里就不展開說明了,下面分析一下 ResultSet 的映射程序,如下:

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) 
throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 創建物體類物件,比如 Article 物件
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null &&
!hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 檢測是否應該自動映射結果集
if (shouldApplyAutomaticMappings(resultMap, false)) {
// 進行自動映射
foundValues = applyAutomaticMappings(
rsw, resultMap, metaObject, null) || foundValues;
 }
// 根據 <resultMap> 節點中配置的映射關系進行映射
foundValues = applyPropertyMappings(
rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue=foundValues || configuration.isReturnInstanceForEmptyRow()
? rowValue : null;
 }
return rowValue; }

上面的方法中的重要邏輯已經注釋出來了,這里再簡單總結一下,如下:

  1. 創建物體類物件
  2. 檢測結果集是否需要自動映射,若需要則進行自動映射
  3. 按中配置的映射關系進行映射
    這三處代碼的邏輯比較復雜,接下來按順序進行分節說明,首先分析物體類的創建程序,

1.創建物體類物件

在我們的印象里,創建物體類物件是一個很簡單的程序,直接通過 new 關鍵字,或通過
反射即可完成任務,大家可能會想,把這么簡單程序也拿出來說說,怕是有湊字數的嫌疑,
實則不然,MyBatis 的維護者寫了不少邏輯,以保證能成功創建物體類物件,如果實在無法
創建,則拋出例外,下面我們來看一下 MyBatis 創建物體類物件的程序,

// -☆- DefaultResultSetHandler
private Object createResultObject(ResultSetWrapper rsw,
ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) 
throws SQLException {
this.useConstructorMappings = false;
final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
final List<Object> constructorArgs = new ArrayList<Object>();
// 呼叫多載方法創建物體類物件
Object resultObject = createResultObject(rsw, 
resultMap, constructorArgTypes, constructorArgs, columnPrefix);
// 檢測物體類是否有相應的型別處理器
if (resultObject != null &&
!hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings =
resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// 如果開啟了延遲加載,則為 resultObject 生成代理類
if (propertyMapping.getNestedQueryId() != null &&
propertyMapping.isLazy()) {
// 創建代理類,默認使用 Javassist 框架生成代理類,由于物體類通常
// 不會實作介面,所以不能使用 JDK 動態代理 API 為物體類生成代理,
resultObject = configuration.getProxyFactory()
 .createProxy(resultObject, lazyLoader, configuration, 
objectFactory,constructorArgTypes, constructorArgs);
break;
 }
 }
 }
this.useConstructorMappings =
resultObject != null && !constructorArgTypes.isEmpty();
return resultObject; }

創建物體類物件的邏輯被封裝在了 createResultObject 的多載方法中,關于該方法,待
會再分析,創建好物體類對后,還需要對中配置的映射資訊進行檢測,若發現
有關聯查詢,且關聯查詢結果的加載方式為延遲加載,此時需為物體類生成代理類,舉個
例子說明一下,假設有如下兩個物體類:

/** 作者類 */
public class Author {
private Integer id;
private String name;
private Integer age;
private Integer sex; }
/** 文章類 */
public class Article {
private Integer id;
private String title;
// 一對一關系
private Author author;
private String content; }

如上,Article 物件中的資料由一條 SQL 從 article 表中查詢,Article 類有一個 author 字
段,該欄位的資料由另一條 SQL 從 author 表中查出,我們在將 article 表的查詢結果填充到
Article 類物件中時,并不希望 MyBaits 立即執行另一條 SQL 查詢 author 欄位對應的資料,
而是期望在我們呼叫 article.getAuthor()方法時,MyBaits 再執行另一條 SQL 從 author 表中查詢出所需的資料,若如此,我們需要改造 getAuthor 方法,以保證呼叫該方法時可讓 MyBaits執行相關的 SQL,關于延遲加載后面將會進行詳細的分析,這里先說這么多,下面分析createResultObject 多載方法的邏輯,如下:

private Object createResultObject(ResultSetWrapper rsw, ResultMap
resultMap, List<Class<?>> constructorArgTypes, List<Object>
constructorArgs, String columnPrefix) throws SQLException {
final Class<?> resultType = resultMap.getType();
final MetaClass metaType =
MetaClass.forClass(resultType, reflectorFactory);
// 獲取 <constructor> 節點對應的 ResultMapping
final List<ResultMapping> constructorMappings =
resultMap.getConstructorResultMappings();
// 檢測是否有與回傳值型別相對應的 TypeHandler,若有則直接從
// 通過 TypeHandler 從結果集中?取資料,并生成回傳值物件
if (hasTypeHandlerForResultObject(rsw, resultType)) {
// 通過 TypeHandler 獲取?取,并生成回傳值物件
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
 } else if (!constructorMappings.isEmpty()) {
// 通過 <constructor> 節點配置的映射資訊從 ResultSet 中?取資料,
// 然后將這些資料傳給指定構造方法,即可創建物體類物件
return createParameterizedResultObject(rsw, resultType, 
constructorMappings, constructorArgTypes, 
constructorArgs, columnPrefix);
 } else if(resultType.isInterface() || metaType.hasDefaultConstructor()){
// 通過 ObjectFactory 呼叫目標類的默認構造方法創建實體
return objectFactory.create(resultType);
 } else if (shouldApplyAutomaticMappings(resultMap, false)) {
 // 通過自動映射查找合適的構造方法創建實體
return createByConstructorSignature(rsw, resultType, 
constructorArgTypes, constructorArgs, columnPrefix);
 }
throw new ExecutorException("……");
}

createResultObject 方法中包含了 4 種創建物體類物件的方式,一般情況下,若無特殊要
求,MyBatis 會通過 ObjectFactory 呼叫默認構造方法創建物體類物件,ObjectFactory 是一個介面,大家可以實作這個介面,以按照自己的邏輯控制物件的創建程序,至此,物體類物件創建好了,接下里要做的事情是將結果集中的資料映射到物體類物件中,

2.結果集映射

在 MyBatis 中,結果集自動映射有三種等級,這三種等級官方檔案上有所說明,這里直
接參考一下,如下:

  • NONE - 禁用自動映射,僅設定手動映射屬性
  • PARTIAL - 將自動映射結果除了那些有內部定義內嵌結果映射的(joins)
  • FULL - 自動映射所有

除了以上三種等級,我們還可以顯示配置節點的 autoMapping 屬性,以啟用
或者禁用指定 ResultMap 的自動映射設定,下面,來看一下自動映射相關的邏輯,

private boolean shouldApplyAutomaticMappings(
ResultMap resultMap, boolean isNested) {
// 檢測 <resultMap> 是否配置了 autoMapping 屬性
if (resultMap.getAutoMapping() != null) {
// 回傳 autoMapping 屬性
return resultMap.getAutoMapping();
 } else {
if (isNested) {
// 對于嵌套 resultMap,僅當全域的映射行為為 FULL 時,才進行自動映射
return AutoMappingBehavior.FULL ==
configuration.getAutoMappingBehavior();
} else {
// 對于普通的 resultMap,只要全域的映射行為不為 NONE,即可進行自動映射
return AutoMappingBehavior.NONE !=
configuration.getAutoMappingBehavior();
 }
 } }

shouldApplyAutomaticMappings 方法用于檢測是否應為當前結果集應用自動映射,檢測
結果取決于節點的 autoMapping 屬性,以及全域自動映射行為,上面代碼的邏輯
不難理解,就不多說了,下面來分析 MyBatis 是如何進行自動映射的,

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap
resultMap, MetaObject metaObject, String columnPrefix) 
throws SQLException {
// 獲取 UnMappedColumnAutoMapping 串列
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(
rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
for (UnMappedColumnAutoMapping mapping : autoMapping) {
// 通過 TypeHandler 從結果集中獲取指定列的資料
final Object value = mapping.typeHandler
.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
 }
if (value != null || (configuration.isCallSettersOnNulls() &&
!mapping.primitive)) {
// 通過元資訊物件設定 value 到物體類物件的指定欄位上
metaObject.setValue(mapping.property, value);
}
 }
 }
return foundValues; }

applyAutomaticMappings 方法的代碼不多,邏輯也不是很復雜,首先是獲取
UnMappedColumnAutoMapping 集合,然后遍歷該集合,并通過 TypeHandler 從結果集中獲取資料,最后再將獲取到的資料設定到物體類物件中,雖然邏輯上看起來沒什么復雜的東西,但如果不清楚 UnMappedColumnAutoMapping 的用途,是無法理解上面代碼的邏輯的,所以這里簡單介紹一下 UnMappedColumnAutoMapping 的用途,UnMappedColumnAutoMapping用于記錄未配置在節點中的映射關系,該類定義在 DefaultResultSetHandler 內部,它的代碼如下:

private static class UnMappedColumnAutoMapping {
private final String column;
private final String property;
private final TypeHandler<?> typeHandler;
private final boolean primitive;
public UnMappedColumnAutoMapping(String column, String property, 
TypeHandler<?> typeHandler, boolean primitive) {
this.column = column;
this.property = property;
this.typeHandler = typeHandler;
this.primitive = primitive;
 } }

以上就是 UnMappedColumnAutoMapping 類的所有代碼,沒什么邏輯,僅用于記錄映射
關系,下面看一下獲取 UnMappedColumnAutoMapping 集合的程序,

// -☆- DefaultResultSetHandler
private List<UnMappedColumnAutoMapping> createAutomaticMappings(
ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, 
String columnPrefix) throws SQLException {
final String mapKey = resultMap.getId() + ":" + columnPrefix;
// 從快取中獲取 UnMappedColumnAutoMapping 串列
List<UnMappedColumnAutoMapping> autoMapping =
autoMappingsCache.get(mapKey);
// 快取未命中
if (autoMapping == null) {
autoMapping = new ArrayList<UnMappedColumnAutoMapping>();
// 從 ResultSetWrapper 中獲取未配置在 <resultMap> 中的列名
final List<String> unmappedColumnNames =
rsw.getUnmappedColumnNames(resultMap, columnPrefix);
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
if (columnPrefix != null && !columnPrefix.isEmpty()) {
if (columnName.toUpperCase(Locale.ENGLISH) .startsWith(columnPrefix)) {
// 獲取不包含列名前綴的屬性名
propertyName =
columnName.substring(columnPrefix.length());
 } else {
continue;
 }
 }
// 將下劃線形式的列名轉成駝峰式,比如 AUTHOR_NAME -> authorName
final String property = metaObject.findProperty(
propertyName, configuration.isMapUnderscoreToCamelCase());
if (property != null && metaObject.hasSetter(property)) {
// 檢測當前屬性是否存在于 resultMap 中
if (resultMap.getMappedProperties().contains(property)) {
continue;
 }
 // 獲取屬性對于的型別
final Class<?> propertyType =
metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(
propertyType, rsw.getJdbcType(columnName))) {
// 獲取型別處理器
final TypeHandler<?> typeHandler =
rsw.getTypeHandler(propertyType, columnName);
// 封裝上面獲取到的資訊到 UnMappedColumnAutoMapping 物件中
autoMapping.add(new UnMappedColumnAutoMapping(
columnName, property, typeHandler, 
propertyType.isPrimitive()));
 } else {
configuration.getAutoMappingUnknownColumnBehavior()
 .doAction(mappedStatement, 
columnName, property, propertyType);
 }
 } else {
// 若 property 為空,或物體類中無 property 屬性,此時無法完成
// 列名與物體類屬性建立映射關系,針對這種情況,有三種處理方式,
// 1. 什么都不做
// 2. 僅列印日志
// 3. 拋出例外
// 默認情況下,是什么都不做
configuration.getAutoMappingUnknownColumnBehavior()
 .doAction(mappedStatement, columnName, 
(property != null) ? property : propertyName, null);
 }
 }
// 寫入快取
autoMappingsCache.put(mapKey, autoMapping);
 }
return autoMapping; }

上面的代碼有點多,不過不用太擔心,耐心看一下,還是可以看懂的,下面總結一下這
個方法的邏輯,

  1. 從 ResultSetWrapper 中獲取未配置在中的列名
  2. 遍歷上一步獲取到的列名串列
  3. 若列名包含列名前綴,則移除列名前綴,得到屬性名
  4. 將下劃線形式的列名轉成駝峰式
  5. 獲取屬性型別
  6. 獲取型別處理器
  7. 創建 UnMappedColumnAutoMapping 實體

以上步驟中,除了第一步,其他都是常規操作,無需過多說明,下面來分析第一個步
驟的邏輯,如下:

// -☆- ResultSetWrapper
public List<String> getUnmappedColumnNames(ResultMap resultMap, 
String columnPrefix) throws SQLException {
List<String> unMappedColumnNames = unMappedColumnNamesMap.get(
getMapKey(resultMap, columnPrefix));
if (unMappedColumnNames == null) {
// 加載已映射與未映射列名
loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
// 獲取未映射列名
unMappedColumnNames = unMappedColumnNamesMap.get(
getMapKey(resultMap, columnPrefix));
 }
return unMappedColumnNames; }
private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, 
String columnPrefix) throws SQLException {
List<String> mappedColumnNames = new ArrayList<String>();
List<String> unmappedColumnNames = new ArrayList<String>();
final String upperColumnPrefix = columnPrefix == null ?
null : columnPrefix.toUpperCase(Locale.ENGLISH);
// 為 <resultMap> 中的列名拼接前綴
final Set<String> mappedColumns = prependPrefixes(
resultMap.getMappedColumns(), upperColumnPrefix);
// 遍歷 columnNames,columnNames 是 ResultSetWrapper 的成員變數,
// 保存了當前結果集中的所有列名
for (String columnName : columnNames) {
final String upperColumnName =
columnName.toUpperCase(Locale.ENGLISH);
// 檢測已映射列名集合中是否包含當前列名
if (mappedColumns.contains(upperColumnName)) {
mappedColumnNames.add(upperColumnName);
 } else {
// 將列名存入 unmappedColumnNames 中
unmappedColumnNames.add(columnName);
 }
 }
// 快取列名集合
mappedColumnNamesMap.put(
getMapKey(resultMap, columnPrefix), mappedColumnNames);
unMappedColumnNamesMap.put(
getMapKey(resultMap, columnPrefix), unmappedColumnNames);
}

如上,已映射列名與未映射列名的分揀邏輯并不復雜,這里簡述一下相關邏輯,首先是
從當前資料集中獲取列名集合,然后獲取中配置的列名集合,之后遍歷資料集中
的列名集合,并判斷列名是否被配置在了節點中,若配置了,則表明該列名已有
映射關系,此時該列名存入 mappedColumnNames 中,若未配置,則表明列名未與物體類的某個欄位形成映射關系,此時該列名存入 unmappedColumnNames 中,這樣,列名的分揀作業就完成了,分揀程序示意圖如下:
在這里插入圖片描述
如上圖所示,物體類 Author 的 id 和 name 欄位與列名 id 和 name 被配置在了<resultMap>
中,它們之間形成了映射關系,列名 age、sex 和 email 未配置在<resultMap>中,因此未與Author 中的欄位形成映射,所以他們最終都被放入了 unMappedColumnNames 集合中,弄懂了未映射列名獲取的程序,自動映射的代碼邏輯就不難懂了,好了,關于自動映射的分析就先到這,接下來分析一下 MyBatis 是如何將結果集中的資料填充到已映射的物體類欄位中的,

// -☆- DefaultResultSetHandler
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap
resultMap, MetaObject metaObject,ResultLoaderMap lazyLoader, String
columnPrefix) throws SQLException {
// 獲取已映射的列名
final List<String> mappedColumnNames =
rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
// 獲取 ResultMapping
final List<ResultMapping> propertyMappings =
resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// 拼接列名前綴,得到完整列名
String column = prependPrefix(
propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
column = null;
 }
/*
* 下面的 if 分支由三個或條件組合而成,三個條件的含義如下:
* 條件一:檢測 column 是否為 {prop1=col1, prop2=col2} 形式,該
* 種形式的 column 一般用于關聯查詢
* 條件二:檢測當前列名是否被包含在已映射的列名集合中,
* 若包含則可進行資料集映射操作
* 條件三:多結果集相關,暫不分析
*/
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(
column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
// 從結果集中獲取指定列的資料
Object value = getPropertyMappingValue(rsw.getResultSet(), 
metaObject, propertyMapping, lazyLoader, columnPrefix);
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
// 若獲取到的值為 DEFERED,則延遲加載該值
 } else if (value == DEFERED) {
foundValues = true;
continue;
 }
if (value != null) {
foundValues = true;
 }
if (value != null || (configuration.isCallSettersOnNulls() &&
!metaObject.getSetterType(property).isPrimitive())) {
// 將獲取到的值設定到物體類物件中
metaObject.setValue(property, value);
 }
 }
 }
return foundValues; }
private Object getPropertyMappingValue(ResultSet rs, MetaObject
metaResultObject, ResultMapping propertyMapping, ResultLoaderMap
lazyLoader, String columnPrefix) throws SQLException {
if (propertyMapping.getNestedQueryId() != null) {
// 獲取關聯查詢結果,下一節分析
return getNestedQueryMappingValue(rs, metaResultObject, 
propertyMapping, lazyLoader, columnPrefix);
 } else if (propertyMapping.getResultSet() != null) {
addPendingChildRelation(rs, metaResultObject, propertyMapping);
return DEFERED;
 } else {
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
// 拼接前綴
final String column = prependPrefix(propertyMapping.getColumn(), 
columnPrefix);
// 從 ResultSet 中獲取指定列的值
return typeHandler.getResult(rs, column);
 } }

applyPropertyMappings 方法首先從 ResultSetWrapper 中獲取已映射列名集合
mappedColumnNames, 從 ResultMap 獲取映射物件 ResultMapping 集合,然后遍歷
ResultMapping 集合,在此程序中呼叫 getPropertyMappingValue 獲取指定指定列的資料,最后將獲取到的資料設定到物體類物件中,到此,基本的結果集映射程序就分析完了,

3.關聯查詢與延遲加載

我們在學習 MyBatis 框架時,會經常碰到一對一,一對多的使用場景,對于這樣的場景,
通常我們可以用一條 SQL 進行多表查詢完成任務,當然我們也可以使用關聯查詢,將一條
SQL 拆成兩條去完成查詢任務,MyBatis 提供了兩個標簽用于支持一對一和一對多的使用場
景,分別是和,下面我來演示一下如何使用完成一對一的關聯查詢,先來看看物體類的定義:

/** 作者類 */
public class Author {
private Integer id;
private String name;
private Integer age;
private Integer sex;
private String email;
// 省略 getter/setter
}
/** 文章類 */
public class Article {
private Integer id;
private String title;
// 一對一關系
private Author author;
private String content;
private Date createTime;
// 省略 getter/setter
}

相關表記錄如下
在這里插入圖片描述
接下來看一下 Mapper 介面與映射檔案的定義,

public interface ArticleDao {
Article findOne(@Param("id") int id);
Author findAuthor(@Param("id") int authorId);
}
<mapper namespace="xyz.coolblog.chapter4.dao.ArticleDao">
<resultMap id="articleResult" type="Article">
<result property="createTime" column="create_time"/>
<association property="author" column="author_id"
javaType="Author" select="findAuthor"/>
</resultMap>
<select id="findOne" resultMap="articleResult">
 SELECT
 id, author_id, title, content, create_time
 FROM
 article
 WHERE
 id = #{id}
</select>
<select id="findAuthor" resultType="Author">
 SELECT
 id, name, age, sex, email
 FROM
 author
 WHERE
 id = #{id}
</select>
</mapper>

好了,必要在的準備作業做完了,下面可以寫測驗代碼了,如下:

public class OneToOneTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "chapter4/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
 }
@Test
public void testOne2One() {
SqlSession session = sqlSessionFactory.openSession();
try {
ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
Author author = article.getAuthor();
article.setAuthor(null);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);
 } finally {
session.close();
 }
 } }

測驗結果如下:
在這里插入圖片描述

如上,從上面的輸出結果中可以看出,我們在呼叫 ArticleDao 的 findOne 方法時,MyBatis
執行了兩條 SQL,完成了一對一的查詢需求,理解了上面的例子后,下面就可以深入到原始碼
中,看看 MyBatis 是如何實作關聯查詢的,接下里從 getNestedQueryMappingValue 方法開始分析,如下:

private Object getNestedQueryMappingValue(ResultSet rs, 
MetaObject metaResultObject, ResultMapping propertyMapping, 
ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// 獲取關聯查詢 id,id = 命名空間 + <association> 的 select 屬性值
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
// 根據 nestedQueryId 獲取 MappedStatement
final MappedStatement nestedQuery =
configuration.getMappedStatement(nestedQueryId);
final Class<?> nestedQueryParameterType =
nestedQuery.getParameterMap().getType();
/*
* 生成關聯查詢陳述句引數物件,引數型別可能是一些包裝類,Map 或是自定義的物體類,
* 具體型別取決于配置資訊,以上面的例子為基礎,下面分析不同配置對
* 引數型別的影響:
* 1. <association column="author_id"> 
* column 屬性值僅包含列資訊,引數型別為 author_id 列對應的型別,
* 這里為 Integer
* 2. <association column="{id=author_id, name=title}"> 
* column 屬性值包含了屬性名與列名的復合資訊,MyBatis 會根據列名從
* ResultSet 中獲取列資料,并將列資料設定到物體類物件的指定屬性中,比如:
* Author{id=1, name="MyBatis 原始碼分析系列文章導讀", age=null, …}
* 或是以鍵值對 <屬性, 列資料> 的形式,將兩者存入 Map 中,比如:
* {"id": 1, "name": "MyBatis 原始碼分析系列文章導讀"}
*
* 至于引數型別到底為物體類還是 Map,取決于關聯查詢陳述句的配置資訊,比如:
* <select id="findAuthor"> -> 引數型別為 Map
* <select id="findAuthor" parameterType="Author"> 
* -> 引數型別為物體類
*/
final Object nestedQueryParameterObject=prepareParameterForNestedQuery(
rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {
// 獲取 BoundSql
final BoundSql nestedBoundSql =
nestedQuery.getBoundSql(nestedQueryParameterObject);
final CacheKey key = executor.createCacheKey(nestedQuery, 
nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
final Class<?> targetType = propertyMapping.getJavaType();
// 檢查一級快取是否保存了關聯查詢結果
if (executor.isCached(nestedQuery, key)) {
// 從一級快取中獲取關聯查詢的結果,并通過 metaResultObject 
// 將結果設定到相應的物體類物件中
executor.deferLoad(nestedQuery, 
metaResultObject, property, key, targetType);
value = DEFERED;
 } else {
// 創建結果加載器
final ResultLoader resultLoader = new ResultLoader(
configuration, executor, nestedQuery, 
nestedQueryParameterObject, targetType, key, nestedBoundSql);
// 檢測當前屬性是否需要延遲加載
if (propertyMapping.isLazy()) {
// 添加延遲加載相關的物件到 loaderMap 集合中
lazyLoader.addLoader(
property, metaResultObject, resultLoader);
value = DEFERED;
 } else {
// 直接執行關聯查詢
value = resultLoader.loadResult();
 }
 }
 }
return value; }

上面對關聯查詢進行了比較多的注釋,導致該方法看起來有點復雜,當然,真實的邏輯
確實有點復雜,因為它還呼叫了其他的很多方法,下面先來總結一下該方法的邏輯:

  1. 根據 nestedQueryId 獲取 MappedStatement
  2. 生成引數物件
  3. 獲取 BoundSql
  4. 檢測一級快取中是否有關聯查詢的結果,若有,則將結果設定到物體類物件中
  5. 若一級快取未命中,則創建結果加載器 ResultLoader
  6. 檢測當前屬性是否需要進行延遲加載,若需要,則添加延遲加載相關的物件到
    loaderMap 集合中
  7. 如不需要延遲加載,則直接通過結果加載器加載結果

如上,getNestedQueryMappingValue 方法中邏輯多是都是和延遲加載有關,除了延遲加
載,以上流程中針對一級快取的檢查是十分有必要的,若快取命中,可直接取用結果,無需
再在執行關聯查詢 SQL,若快取未命中,接下來就要按部就班執行延遲加載相關邏輯,接下
來,分析一下 MyBatis 延遲加載是如何實作的,首先我們來看一下添加延遲加載相關物件到
loaderMap 集合中的邏輯,如下:

// -☆- ResultLoaderMap
public void addLoader(String property, MetaObject metaResultObject, 
ResultLoader resultLoader) {
// 將屬性名轉為大寫
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) &&
loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("……");
 }
// 創建 LoadPair,并將 <大寫屬性名,LoadPair 物件> 鍵值對添加到 loaderMap 中
loaderMap.put(upperFirst, 
new LoadPair(property, metaResultObject, resultLoader));
}

addLoader 方法的引數最終都傳給了 LoadPair,該類的 load 方法會在內部呼叫
ResultLoader 的 loadResult 方法進行關聯查詢,并通過 metaResultObject 將查詢結果設定到實
體類物件中,那 LoadPair 的 load 方法由誰呼叫呢?答案是物體類的代理物件,下面我們修改一下上面示例中的部分代碼,演示一下延遲加載,首先,我們需要在 MyBatis 組態檔的
<settings>節點中加入或覆寫如下配置:

<!-- 開啟延遲加載 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 關倍訓極的加載策略 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 延遲加載的觸發方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

上面三個配置 MyBatis 官方檔案中有較為詳細的介紹,大家可以參考官方檔案,這里就
不詳細介紹了,下面修改一下測驗類的代碼:

public class OneToOneTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {...}
@Test
public void testOne2One2() {
SqlSession session = sqlSessionFactory.openSession();
try {
ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\n 延遲加載 author 欄位:");
// 通過 getter 方法觸發延遲加載
Author author = article.getAuthor();
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);
 } finally {
session.close();
 }
 } }

測驗結果如下:
在這里插入圖片描述

從上面結果中可以看出,我們在未呼叫 getAuthor 方法時,Article 物件中的 author 欄位
為 null,呼叫該方法后,再次輸出 Article 物件,發現其 author 欄位有值了,表明 author 欄位的延遲加載邏輯被觸發了,既然呼叫 getAuthor 可以觸發延遲加載,那么該方法一定被做過手腳了,不然該方法應該回傳 null 才是,實際情況確實如此,MyBatis 會為需要延遲加載的類生成代理類,代理邏輯會攔截物體類的方法呼叫,默認情況下,MyBatis 會使用 Javassist為物體類生成代理,代理邏輯封裝在 JavassistProxyFactory 類中,下面一起看一下,

// -☆- JavassistProxyFactory
public Object invoke(Object enhanced, Method method, Method methodProxy, 
Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
// 針對 writeReplace 方法的處理邏輯,與延遲加載無關,不分析了
 } else {
if (lazyLoader.size() > 0 &&
!FINALIZE_METHOD.equals(methodName)) {
// 如果 aggressive 為 true,或觸發方法(比如 equals,
// hashCode 等)被呼叫,則加載所有的所有延遲加載的資料
if (aggressive ||
lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
 } else if (PropertyNamer.isSetter(methodName)) {
final String property =
PropertyNamer.methodToProperty(methodName);
// 如果使用者顯示呼叫了 setter 方法,則將相應的
// 延遲加載類從 loaderMap 中移除
lazyLoader.remove(property);
// 檢測使用者是否呼叫 getter 方法
 } else if (PropertyNamer.isGetter(methodName)) {
final String property =
PropertyNamer.methodToProperty(methodName);
// 檢測該屬性是否有相應的 LoadPair 物件
if (lazyLoader.hasLoader(property)) {
// 執行延遲加載邏輯
lazyLoader.load(property);
 }
 }
 }
 }
 }
// 呼叫被代理類的方法
return methodProxy.invoke(enhanced, args);
 } catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
 } }

如上,代理方法首先會檢查 aggressive 是否為 true ,如果不滿足,再去檢查
lazyLoadTriggerMethods 是否包含當前方法名,這里兩個條件只要一個為 true,當前物體類
中所有需要延遲加載,aggressive 和 lazyLoadTriggerMethods 兩個變數的值取決于下面的配置,

<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

回到上面的代碼中,如果執行執行緒未進入第一個條件分支,那么緊接著,代理邏輯會檢
查使用者是不是呼叫了物體類的 setter 方法,如果呼叫了,就將該屬性對應的 LoadPair 從
loaderMap 中移除,為什么要這么做呢?答案是:使用者既然手動呼叫 setter 方法,說明使用者想自定義某個屬性的值,此時,延遲加載邏輯不應該再修改該屬性的值,所以這里從
loaderMap 中移除屬性對于的 LoadPair,最后如果使用者呼叫的是某個屬性的 getter 方法,
且該屬性配置了延遲加載,此時延遲加載邏輯就會被觸發,那接下來,我們來看看延遲加載
邏輯是怎樣實作的的,

// -☆- ResultLoaderMap
public boolean load(String property) throws SQLException {
// 從 loaderMap 中移除 property 所對應的 LoadPair
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
// 加載結果
pair.load();
return true;
 }
return false; }
// -☆- LoadPair
public void load() throws SQLException {
if (this.metaResultObject == null) {
throw new IllegalArgumentException("metaResultObject is null");
 }
if (this.resultLoader == null) {
throw new IllegalArgumentException("resultLoader is null");
 }
// 呼叫多載方法
this.load(null);
}
public void load(final Object userObject) throws SQLException {
// 若 metaResultObject 和 resultLoader 為 null,則創建相關物件,
// 在當前呼叫情況下,兩者均不為 null,條件不成立,篇幅原因,下面代碼不分析了
if (this.metaResultObject == null || this.resultLoader == null) {...}
// 執行緒安全檢測
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
// 重新創建新的 ResultLoader 和 ClosedExecutor,
// ClosedExecutor 是非執行緒安全的
this.resultLoader = new ResultLoader(old.configuration, 
new ClosedExecutor(), old.mappedStatement, old.parameterObject, 
old.targetType, old.cacheKey, old.boundSql);
 }
// 呼叫 ResultLoader 的 loadResult 方法加載結果,
// 并通過 metaResultObject 設定結果到物體類物件中
this.metaResultObject.setValue(property,this.resultLoader.loadResult());
}

上面的代碼比較多,但是沒什么特別的邏輯,我們重點關注最后一行有效代碼就行了,
下面看一下 ResultLoader 的 loadResult 方法邏輯是怎樣的,

public Object loadResult() throws SQLException {
// 執行關聯查詢
List<Object> list = selectList();
// 抽取結果
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject; }
private <E> List<E> selectList() throws SQLException {
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId ||
localExecutor.isClosed()) {
localExecutor = newExecutor();
 }
try {
// 通過 Executor 就行查詢,這個之前已經分析過了
return localExecutor.<E>query(mappedStatement, parameterObject, 
RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey,boundSql);
 } finally {
if (localExecutor != executor) {
localExecutor.close(false);
 }
 } }

如上,我們在 ResultLoader 中終于看到了執行關聯查詢的代碼,即 selectList 方法中的
邏輯,該方法在內部通過 Executor 進行查詢,至于查詢結果的抽取程序,并不是本節所關心
的點,因此大家自行分析吧,到此,關于關聯查詢與延遲加載就分析完了,

4.存盤映射結果

存盤映射結果是“查詢結果”處理流程中的最后一環,實際上也是查詢陳述句執行程序的最
后一環,本節內容分析完,整個查詢程序就分析完了,那接下來讓我們帶著喜悅的心情來分
析映射結果存盤邏輯,

private void storeObject(ResultHandler<?> resultHandler, 
DefaultResultContext<Object> resultContext,Object rowValue, ResultMapping
parentMapping, ResultSet rs) throws SQLException {
if (parentMapping != null) {
// 多結果集相關,不分析了
linkToParents(rs, parentMapping, rowValue);
 } else {
// 存盤結果
callResultHandler(resultHandler, resultContext, rowValue);
 } }
private void callResultHandler(ResultHandler<?> resultHandler, 
DefaultResultContext<Object> resultContext, Object rowValue) {
// 設定結果到 resultContext 中
resultContext.nextResultObject(rowValue);
// 從 resultContext 獲取結果,并存盤到 resultHandler 中
 ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

上面方法顯示將 rowValue 設定到 ResultContext 中,然后再將 ResultContext 物件作為參
數傳給 ResultHandler 的 handleResult 方法,下面我們分別看一下 ResultContext 和
ResultHandler 的實作類,如下:

public class DefaultResultContext<T> implements ResultContext<T> {
private T resultObject;
private int resultCount;
/** 狀態欄位 */
private boolean stopped;
// 省略部分代碼
@Override
public boolean isStopped() {
return stopped;
 }
public void nextResultObject(T resultObject) {
resultCount++;
this.resultObject = resultObject;
 }
@Override
public void stop() {
this.stopped = true;
 } }

DefaultResultContext 中包含了一個狀態欄位,表明結果背景關系的狀態,在處理多行資料
時,MyBatis 會檢查該欄位的值,已決定是否需要進行后續的處理,該類的邏輯比較簡單,
不多說了,下面再來看一下 DefaultResultHandler 的原始碼,

public class DefaultResultHandler implements ResultHandler<Object> {
private final List<Object> list;
public DefaultResultHandler() {
list = new ArrayList<Object>();
 }
 @Override
public void handleResult(ResultContext<? extends Object> context) {
// 添加結果到 list 中
list.add(context.getResultObject());
 }
public List<Object> getResultList() {
return list;
 } }

如上,DefaultResultHandler 默認使用 List 存盤結果,除此之外,如果 Mapper(或 Dao)
介面方法回傳值為 Map 型別,此時則需要另一種 ResultHandler 實作類處理結果,即
DefaultMapResultHandler,關于 DefaultMapResultHandler 的原始碼大家自行分析吧啊,本節就不展開了,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/247686.html

標籤:其他

上一篇:p1593 因子和

下一篇:陣列實作的單鏈表

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more