MyBatis 源碼分析 - SQL 的執行過程
* 本文速覽
本篇文章較為詳細的介紹了 MyBatis 執行 SQL 的過程。該過程本身比較復雜,牽涉到的技術點比較多。包括但不限于 Mapper 接口代理類的生成、接口方法的解析、SQL 語句的解析、運行時參數的綁定、查詢結果自動映射、延遲加載等。本文對所列舉的技術點,以及部分未列舉的技術點都做了較為詳細的分析。全文篇幅很大,需要大家耐心閱讀。下面來看一下本文的目錄:

源碼分析類文章通常比較枯燥。因此,我在分析源碼的過程中寫了一些示例,同時也繪制了一些圖片。希望通過這些示例和圖片,幫助大家理解 MyBatis 的源碼。

本篇文章篇幅很大,全文字數約 26000 字,閱讀時間預計超過 100 分鐘。通讀本文可能會比較累,大家可以分次閱讀。好了,本文的速覽就先到這,下面進入正文。
1.簡介
在前面的文章中,我分析了配置文件和映射文件的解析過程。經過前面復雜的解析過程后,現在,MyBatis 已經進入了就緒狀態,等待使用者發號施令。本篇文章我將分析MyBatis 執行 SQL 的過程,該過程比較復雜,涉及的技術點很多。包括但不限于以下技術點:
- 為 mapper 接口生成實現類
- 根據配置信息生成 SQL,并將運行時參數設置到 SQL 中
- 一二級緩存的實現
- 插件機制
- 數據庫連接的獲取與管理
- 查詢結果的處理,以及延遲加載等
如果大家能掌握上面的技術點,那么對 MyBatis 的原理將會有很深入的理解。若將以上技術點一一展開分析,會導致文章篇幅很大,因此我打算將以上知識點分成數篇文章進行分析。本篇文章將分析以上列表中的第1個、第2個以及第6個技術點,其他技術點將會在隨后的文章中進行分析。好了,其他的就不多說了,下面開始我們的源碼分析之旅。
2.SQL 執行過程分析
2.1 SQL 執行入口分析
在單獨使用 MyBatis 進行數據庫操作時,我們通常都會先調用 SqlSession 接口的 getMapper 方法為我們的 Mapper 接口生成實現類。然后就可以通過 Mapper 進行數據庫操作。比如像下面這樣:
ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
Article article = articleMapper.findOne(1);
如果大家對 MyBatis 較為理解,會知道 SqlSession 是通過 JDK 動態代理的方式為接口生成代理對象的。在調用接口方法時,方法調用會被代理邏輯攔截。在代理邏輯中可根據方法名及方法歸屬接口獲取到當前方法對應的 SQL 以及其他一些信息,拿到這些信息即可進行數據庫操作。
上面是一個簡版的 SQL 執行過程,省略了很多細節。下面我們先按照這個簡版的流程進行分析,首先我們來看一下 Mapper 接口的代理對象創建過程。
2.1.1 為 Mapper 接口創建代理對象
本節,我們從 DefaultSqlSession 的 getMapper 方法開始看起,如下:
// -☆- DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
// -☆- Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// -☆- MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 從 knownMappers 中獲取與 type 對應的 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 創建代理對象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
如上,經過連續的調用,Mapper 接口代理對象的創建邏輯初現端倪。如果沒看過我前面的分析文章,大家可能不知道 knownMappers 集合中的元素是何時存入的。這里再說一遍吧,MyBatis 在解析配置文件的 <mappers> 節點的過程中,會調用 MapperRegistry 的 addMapper 方法將 Class 到 MapperProxyFactory 對象的映射關系存入到 knownMappers。具體的代碼就不分析了,大家可以閱讀我之前寫的文章,或者自行分析相關的代碼。
在獲取到 MapperProxyFactory 對象后,即可調用工廠方法為 Mapper 接口生成代理對象了。相關邏輯如下:
// -☆- MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
/*
* 創建 MapperProxy 對象,MapperProxy 實現了
* InvocationHandler 接口,代理邏輯封裝在此類中
*/
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 通過 JDK 動態代理創建代理對象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
上面的代碼首先創建了一個 MapperProxy 對象,該對象實現了 InvocationHandler 接口。然后將對象作為參數傳給重載方法,并在重載方法中調用 JDK 動態代理接口為 Mapper 生成代理對象。
到此,關于 Mapper 接口代理對象的創建過程就分析完了。現在我們的 ArticleMapper 接口指向的代理對象已經創建完畢,下面就可以調用接口方法進行數據庫操作了。由于接口方法會被代理邏輯攔截,所以下面我們把目光聚焦在代理邏輯上面,看看代理邏輯會做哪些事情。
2.1.2 執行代理邏輯
在 MyBatis 中,Mapper 接口方法的代理邏輯實現的比較簡單。該邏輯首先會對攔截的方法進行一些檢測,以決定是否執行后續的數據庫操作。對應的代碼如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果方法是定義在 Object 類中的,則直接調用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
/*
* 下面的代碼最早出現在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
* 新特性 - 默認方法。這段代碼的邏輯就不分析了,有興趣的同學可以
* 去 Github 上看一下相關的相關的討論(issue #709),鏈接如下:
*
* https://github.com/mybatis/mybatis-3/issues/709
*/
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 從緩存中獲取 MapperMethod 對象,若緩存未命中,則創建 MapperMethod 對象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 調用 execute 方法執行 SQL
return mapperMethod.execute(sqlSession, args);
}
如上,代理邏輯會首先檢測被攔截的方法是不是定義在 Object 中的,比如 equals、hashCode 方法等。對于這類方法,直接執行即可。除此之外,MyBatis 從 3.4.2 版本開始,對 JDK 1.8 接口的默認方法提供了支持,具體就不分析了。完成相關檢測后,緊接著從緩存中獲取或者創建 MapperMethod 對象,然后通過該對象中的 execute 方法執行 SQL。在分析 execute 方法之前,我們先來看一下 MapperMethod 對象的創建過程。MapperMethod 的創建過程看似普通,但卻包含了一些重要的邏輯,所以不能忽視。
2.1.2.1 創建 MapperMethod 對象
本節來分析一下 MapperMethod 的構造方法,看看它的構造方法中都包含了哪些邏輯。如下:
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 創建 SqlCommand 對象,該對象包含一些和 SQL 相關的信息
this.command = new SqlCommand(config, mapperInterface, method);
// 創建 MethodSignature 對象,從類名中可知,該對象包含了被攔截方法的一些信息
this.method = new MethodSignature(config, mapperInterface, method);
}
}
如上,MapperMethod 構造方法的邏輯很簡單,主要是創建 SqlCommand 和 MethodSignature 對象。這兩個對象分別記錄了不同的信息,這些信息在后續的方法調用中都會被用到。下面我們深入到這兩個類的構造方法中,探索它們的初始化邏輯。
① 創建 SqlCommand 對象
前面說了 SqlCommand 中保存了一些和 SQL 相關的信息,那具體有哪些信息呢?答案在下面的代碼中。
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 解析 MappedStatement
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
// 檢測當前方法是否有對應的 MappedStatement
if (ms == null) {
// 檢測當前方法是否有 @Flush 注解
if (method.getAnnotation(Flush.class) != null) {
// 設置 name 和 type 遍歷
name = null;
type = SqlCommandType.FLUSH;
} else {
/*
* 若 ms == null 且方法無 @Flush 注解,此時拋出異常。
* 這個異常比較常見,大家應該眼熟吧
*/
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 設置 name 和 type 變量
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
}
如上,SqlCommand 的構造方法主要用于初始化它的兩個成員變量。代碼不是很長,邏輯也不難理解,就不多說了。繼續往下看。
② 創建 MethodSignature 對象
MethodSignature 即方法簽名,顧名思義,該類保存了一些和目標方法相關的信息。比如目標方法的返回類型,目標方法的參數列表信息等。下面,我們來分析一下 MethodSignature 的構造方法。
public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final boolean returnsCursor;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 通過反射解析方法返回類型
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
// 檢測返回值類型是否是 void、集合或數組、Cursor、Map 等
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
// 解析 @MapKey 注解,獲取注解內容
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
/*
* 獲取 RowBounds 參數在參數列表中的位置,如果參數列表中
* 包含多個 RowBounds 參數,此方法會拋出異常
*/
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
// 獲取 ResultHandler 參數在參數列表中的位置
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
// 解析參數列表
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
}
上面的代碼用于檢測目標方法的返回類型,以及解析目標方法參數列表。其中,檢測返回類型的目的是為避免查詢方法返回錯誤的類型。比如我們要求接口方法返回一個對象,結果卻返回了對象集合,這會導致類型轉換錯誤。關于返回值類型的解析過程先說到這,下面分析參數列表的解析過程。
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
private final SortedMap<Integer, String> names;
public ParamNameResolver(Configuration config, Method method) {
// 獲取參數類型列表
final Class<?>[] paramTypes = method.getParameterTypes();
// 獲取參數注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 檢測當前的參數類型是否為 RowBounds 或 ResultHandler
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 獲取 @Param 注解內容
name = ((Param) annotation).value();
break;
}
}
// name 為空,表明未給參數配置 @Param 注解
if (name == null) {
// 檢測是否設置了 useActualParamName 全局配置
if (config.isUseActualParamName()) {
/*
* 通過反射獲取參數名稱。此種方式要求 JDK 版本為 1.8+,
* 且要求編譯時加入 -parameters 參數,否則獲取到的參數名
* 仍然是 arg1, arg2, ..., argN
*/
name = getActualParamName(method, paramIndex);
}
if (name == null) {
/*
* 使用 map.size() 返回值作為名稱,思考一下為什么不這樣寫:
* name = String.valueOf(paramIndex);
* 因為如果參數列表中包含 RowBounds 或 ResultHandler,這兩個參數
* 會被忽略掉,這樣將導致名稱不連續。
*
* 比如參數列表 (int p1, int p2, RowBounds rb, int p3)
* - 期望得到名稱列表為 ["0", "1", "2"]
* - 實際得到名稱列表為 ["0", "1", "3"]
*/
name = String.valueOf(map.size());
}
}
// 存儲 paramIndex 到 name 的映射
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
}
以上就是方法參數列表的解析過程,解析完畢后,可得到參數下標到參數名的映射關系,這些映射關系最終存儲在 ParamNameResolver 的 names 成員變量中。這些映射關系將會在后面的代碼中被用到,大家留意一下。
下面寫點代碼測試一下 ParamNameResolver 的解析邏輯。如下:
public class ParamNameResolverTest {
@Test
public void test() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
Configuration config = new Configuration();
config.setUseActualParamName(false);
Method method = ArticleMapper.class.getMethod("select", Integer.class, String.class, RowBounds.class, Article.class);
ParamNameResolver resolver = new ParamNameResolver(config, method);
Field field = resolver.getClass().getDeclaredField("names");
field.setAccessible(true);
// 通過反射獲取 ParamNameResolver 私有成員變量 names
Object names = field.get(resolver);
System.out.println("names: " + names);
}
class ArticleMapper {
public void select(@Param("id") Integer id, @Param("author") String author, RowBounds rb, Article article) {}
}
}
測試結果如下:

參數索引與名稱映射圖如下:

到此,關于 MapperMethod 的初始化邏輯就分析完了,繼續往下分析。
2.1.2.2 執行 execute 方法
前面已經分析了 MapperMethod 的初始化過程,現在 MapperMethod 創建好了。那么,接下來要做的事情是調用 MapperMethod 的 execute 方法,執行 SQL。代碼如下:
// -☆- MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根據 SQL 類型執行相應的數據庫操作
switch (command.getType()) {
case INSERT: {
// 對用戶傳入的參數進行轉換,下同
Object param = method.convertArgsToSqlCommandParam(args);
// 執行插入操作,rowCountResult 方法用于處理返回值
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
// 執行更新操作
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
// 執行刪除操作
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 根據目標方法的返回類型進行相應的查詢操作
if (method.returnsVoid() && method.hasResultHandler()) {
/*
* 如果方法返回值為 void,但參數列表中包含 ResultHandler,表明使用者
* 想通過 ResultHandler 的方式獲取查詢結果,而非通過返回值獲取結果
*/
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 執行查詢操作,并返回多個結果
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 執行查詢操作,并將結果封裝在 Map 中返回
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 執行查詢操作,并返回一個 Cursor 對象
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
// 執行查詢操作,并返回一個結果
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
// 執行刷新操作
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 如果方法的返回值為基本類型,而返回值卻為 null,此種情況下應拋出異常
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType()
+ ").");
}
return result;
}
如上,execute 方法主要由一個 switch 語句組成,用于根據 SQL 類型執行相應的數據庫操作。該方法的邏輯清晰,不需要太多的分析。不過在上面的方法中 convertArgsToSqlCommandParam 方法出現次數比較頻繁,這里分析一下:
// -☆- MapperMethod
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
/*
* 如果方法參數列表無 @Param 注解,且僅有一個非特別參數,則返回該參數的值。
* 比如如下方法:
* List findList(RowBounds rb, String name)
* names 如下:
* names = {1 : "0"}
* 此種情況下,返回 args[names.firstKey()],即 args[1] -> name
*/
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 添加 <參數名, 參數值> 鍵值對到 param 中
param.put(entry.getValue(), args[entry.getKey()]);
// genericParamName = param + index。比如 param1, param2, ... paramN
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
/*
* 檢測 names 中是否包含 genericParamName,什么情況下會包含?答案如下:
*
* 使用者顯式將參數名稱配置為 param1,即 @Param("param1")
*/
if (!names.containsValue(genericParamName)) {
// 添加 <param*, value> 到 param 中
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
如上,convertArgsToSqlCommandParam 是一個空殼方法,該方法最終調用了 ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要邏輯是根據條件返回不同的結果,該方法的代碼不是很難理解,我也進行了比較詳細的注釋,就不多說了。
分析完 convertArgsToSqlCommandParam 的邏輯,接下來說說 MyBatis 對哪些 SQL 指令提供了支持,如下:
- 查詢語句:SELECT
- 更新語句:INSERT/UPDATE/DELETE
- 存儲過程:CALL
在上面的列表中,我刻意對 SELECT/INSERT/UPDATE/DELETE 等指令進行了分類,分類依據指令的功能以及 MyBatis 執行這些指令的過程。這里把 SELECT 稱為查詢語句,INSERT/UPDATE/DELETE 等稱為更新語句。接下來,先來分析查詢語句的執行過程。
2.2 查詢語句的執行過程分析
查詢語句對應的方法比較多,有如下幾種:
- executeWithResultHandler
- executeForMany
- executeForMap
- executeForCursor
這些方法在內部調用了 SqlSession 中的一些 select* 方法,比如 selectList、selectMap、selectCursor 等。這些方法的返回值類型是不同的,因此對于每種返回類型,需要有專門的處理方法。以 selectList 方法為例,該方法的返回值類型為 List。但如果我們的 Mapper 或 Dao 的接口方法返回值類型為數組,或者 Set,直接將 List 類型的結果返回給 Mapper/Dao 就不合適了。execute* 等方法只是對 select* 等方法做了一層簡單的封裝,因此接下來我們應該把目光放在這些 select* 方法上。下面我們來分析一下 selectOne 方法的源碼,如下:
2.2.1 selectOne 方法分析
本節選擇分析 selectOne 方法,而不是其他的方法,大家或許會覺得奇怪。前面提及了 selectList、selectMap、selectCursor 等方法,這里卻分析一個未提及的方法。這樣做并沒什么特別之處,主要原因是 selectOne 在內部會調用 selectList 方法。這里分析 selectOne 方法是為了告知大家,selectOne 和 selectList 方法是有聯系的,同時分析 selectOne 方法等同于分析 selectList 方法。如果你不信的話,那我們看源碼吧,源碼面前了無秘密。
// -☆- DefaultSqlSession
public <T> T selectOne(String statement, Object parameter) {
// 調用 selectList 獲取結果
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
// 返回結果
return list.get(0);
} else if (list.size() > 1) {
// 如果查詢結果大于1則拋出異常,這個異常也是很常見的
throw new TooManyResultsException(
"Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
如上,selectOne 方法在內部調用 selectList 了方法,并取 selectList 返回值的第1個元素作為自己的返回值。如果 selectList 返回的列表元素大于1,則拋出異常。上面代碼比較易懂,就不多說了。下面我們來看看 selectList 方法的實現。
// -☆- DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter) {
// 調用重載方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
private final Executor executor;
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 獲取 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 調用 Executor 實現類中的 query 方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
如上,這里要來說說 executor 變量,該變量類型為 Executor。Executor 是一個接口,它的實現類如下:

如上,Executor 有這么多的實現類,大家猜一下 executor 變量對應哪個實現類。要弄清楚這個問題,需要大家到源頭去查證。這里提示一下,大家可以跟蹤一下 DefaultSqlSessionFactory 的 openSession 方法,很快就能發現executor 變量創建的蹤跡。限于篇幅原因,本文就不分析 openSession 方法的源碼了。好了,下面我來直接告訴大家 executor 變量對應哪個實現類吧。默認情況下,executor 的類型為 CachingExecutor,該類是一個裝飾器類,用于給目標 Executor 增加二級緩存功能。那目標 Executor 是誰呢?默認情況下是 SimpleExecutor。
現在大家搞清楚 executor 變量的身份了,接下來繼續分析 selectOne 方法的調用棧。先來看看 CachingExecutor 的 query 方法是怎樣實現的。如下:
// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 獲取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 創建 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 調用重載方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
上面的代碼用于獲取 BoundSql 對象,創建 CacheKey 對象,然后再將這兩個對象傳給重載方法。關于 BoundSql 的獲取過程較為復雜,我將在下一節進行分析。CacheKey 以及接下來即將出現的一二級緩存將會獨立成文進行分析。
上面的方法和 SimpleExecutor 父類 BaseExecutor 中的實現沒什么區別,有區別的地方在于這個方法所調用的重載方法。我們繼續往下看。
// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 從 MappedStatement 中獲取緩存
Cache cache = ms.getCache();
// 若映射文件中未配置緩存或參照緩存,此時 cache = null
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 若緩存未命中,則調用被裝飾類的 query 方法
list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 調用被裝飾類的 query 方法
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
上面的代碼涉及到了二級緩存,若二級緩存為空,或未命中,則調用被裝飾類的 query 方法。下面來看一下 BaseExecutor 的中簽名相同的 query 方法是如何實現的。
// -☆- BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 從一級緩存中獲取緩存項
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 存儲過程相關處理邏輯,本文不分析存儲過程,故該方法不分析了
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 一級緩存未命中,則從數據庫中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
// 從一級緩存中延遲加載嵌套查詢結果
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
如上,上面的方法主要用于從一級緩存中查找查詢結果。若緩存未命中,再向數據庫進行查詢。在上面的代碼中,出現了一個新的類 DeferredLoad,這個類用于延遲加載。該類的實現并不復雜,但是具體用途讓我有點疑惑。這個我目前也未完全搞清楚,就不強行分析了。接下來,我們來看一下 queryFromDatabase 方法的實現。
// -☆- BaseExecutor
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 向緩存中存儲一個占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 調用 doQuery 進行查詢
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 緩存查詢結果
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
上面的代碼仍然不是 selectOne 方法調用棧的終點,拋開緩存操作,queryFromDatabase 最終還會調用 doQuery 進行查詢。下面我們繼續進行跟蹤。
// -☆- SimpleExecutor
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 創建 StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 創建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行查詢操作
return handler.<E>query(stmt, resultHandler);
} finally {
// 關閉 Statement
closeStatement(stmt);
}
}
上面的方法中仍然有不少的邏輯,完全看不到即將要到達終點的趨勢,不過這離終點又近了一步。接下來,我們先跳過 StatementHandler 和 Statement 創建過程,這兩個對象的創建過程會在后面進行說明。這里,我們以 PreparedStatementHandler 為例,看看它的 query 方法是怎樣實現的。如下:
// -☆- PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行 SQL
ps.execute();
// 處理執行結果
return resultSetHandler.<E>handleResultSets(ps);
}
到這里似乎看到了希望,整個調用過程總算要結束了。不過先別高興的太早,SQL 執行結果的處理過程也很復雜,稍后將會專門拿出一節內容進行分析。
以上就是 selectOne 方法的執行過程,盡管我已經簡化了代碼分析,但是整個過程看起來還是很復雜的。查詢過程涉及到了很多方法調用,不把這些調用方法搞清楚,很難對 MyBatis 的查詢過程有深入的理解。所以在接下來的章節中,我將會對一些重要的調用進行分析。如果大家不滿足于泛泛而談,那么接下來咱們一起進行更為深入的探索吧。
2.2.2 獲取 BoundSql
我們在執行 SQL 時,一個重要的任務是將 SQL 語句解析出來。我們都知道 SQL 是配置在映射文件中的,但由于映射文件中的 SQL 可能會包含占位符 #{},以及動態 SQL 標簽,比如 <if>、<where> 等。因此,我們并不能直接使用映射文件中配置的 SQL。MyBatis 會將映射文件中的 SQL 解析成一組 SQL 片段。如果某個片段中也包含動態 SQL 相關的標簽,那么,MyBatis 會對該片段再次進行分片。最終,一個 SQL 配置將會被解析成一個 SQL 片段樹。形如下面的圖片:

我們需要對片段樹進行解析,以便從每個片段對象中獲取相應的內容。然后將這些內容組合起來即可得到一個完成的 SQL 語句,這個完整的 SQL 以及其他的一些信息最終會存儲在 BoundSql 對象中。下面我們來看一下 BoundSql 類的成員變量信息,如下:
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
下面用一個表格列舉各個成員變量的含義。
| 變量名 | 類型 | 用途 |
|---|---|---|
| sql | String | 一個完整的 SQL 語句,可能會包含問號 ? 占位符 |
| parameterMappings | List | 參數映射列表,SQL 中的每個 #{xxx} 占位符都會被解析成相應的 ParameterMapping 對象 |
| parameterObject | Object | 運行時參數,即用戶傳入的參數,比如 Article 對象,或是其他的參數 |
| additionalParameters | Map | 附加參數集合,用于存儲一些額外的信息,比如 datebaseId 等 |
| metaParameters | MetaObject | additionalParameters 的元信息對象 |
以上對 BoundSql 的成員變量做了簡要的說明,部分參數的用途大家現在可能不是很明白。不過不用著急,這些變量在接下來的源碼分析過程中會陸續的出現。到時候對著源碼多思考,或是寫點測試代碼調試一下,即可弄懂。
好了,現在準備工作已經做好。接下來,開始分析 BoundSql 的構建過程。我們源碼之旅的第一站是 MappedStatement 的 getBoundSql 方法,代碼如下:
// -☆- MappedStatement
public BoundSql getBoundSql(Object parameterObject) {
// 調用 sqlSource 的 getBoundSql 獲取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
/*
* 創建新的 BoundSql,這里的 parameterMap 是 ParameterMap 類型。
* 由<ParameterMap> 節點進行配置,該節點已經廢棄,不推薦使用。默認情況下,
* parameterMap.getParameterMappings() 返回空集合
*/
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// 省略不重要的邏輯
return boundSql;
}
如上,MappedStatement 的 getBoundSql 在內部調用了 SqlSource 實現類的 getBoundSql 方法。處理此處的調用,余下的邏輯都不是重要邏輯,就不啰嗦了。接下來,我們把目光轉移到 SqlSource 實現類的 getBoundSql 方法上。SqlSource 是一個接口,它有如下幾個實現類:
- DynamicSqlSource
- RawSqlSource
- StaticSqlSource
- ProviderSqlSource
- VelocitySqlSource
在如上幾個實現類中,我們應該選擇分析哪個實現類的邏輯呢?如果大家分析過 MyBatis 映射文件的解析過程,或者閱讀過我上一篇的關于MyBatis 映射文件分析的文章,那么這個問題不難回答。好了,不賣關子了,我來回答一下這個問題吧。首先我們把最后兩個排除掉,不常用。剩下的三個實現類中,僅前兩個實現類會在映射文件解析的過程中被使用。當 SQL 配置中包含 ${}(不是 #{})占位符,或者包含 <if>、<where> 等標簽時,會被認為是動態 SQL,此時使用 DynamicSqlSource 存儲 SQL 片段。否則,使用 RawSqlSource 存儲 SQL 配置信息。相比之下 DynamicSqlSource 存儲的 SQL 片段類型較多,解析起來也更為復雜一些。因此下面我將分析 DynamicSqlSource 的 getBoundSql 方法。弄懂這個,RawSqlSource 也不在話下。好了,下面開始分析。
// -☆- DynamicSqlSource
public BoundSql getBoundSql(Object parameterObject) {
// 創建 DynamicContext
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 解析 SQL 片段,并將解析結果存儲到 DynamicContext 中
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
/*
* 構建 StaticSqlSource,在此過程中將 sql 語句中的占位符 #{} 替換為問號 ?,
* 并為每個占位符構建相應的 ParameterMapping
*/
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 調用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
如上,DynamicSqlSource 的 getBoundSql 方法的代碼看起來不多,但是邏輯卻并不簡單。該方法由數個步驟組成,這里總結一下:
- 創建 DynamicContext
- 解析 SQL 片段,并將解析結果存儲到 DynamicContext 中
- 解析 SQL 語句,并構建 StaticSqlSource
- 調用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
- 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中
如上5個步驟中,第5步為常規操作,就不多說了,其他步驟將會在接下來章節中一一進行分析。按照順序,我們先來分析 DynamicContext 的實現。
2.2.2.1 DynamicContext
DynamicContext 是 SQL 語句構建的上下文,每個 SQL 片段解析完成后,都會將解析結果存入 DynamicContext 中。待所有的 SQL 片段解析完畢后,一條完整的 SQL 語句就會出現在 DynamicContext 對象中。下面我們來看一下 DynamicContext 類的定義。
public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
public DynamicContext(Configuration configuration, Object parameterObject) {
// 創建 ContextMap
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
// 存放運行時參數 parameterObject 以及 databaseId
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
// 省略部分代碼
}
如上,上面只貼了 DynamicContext 類的部分代碼。其中 sqlBuilder 變量用于存放 SQL 片段的解析結果,bindings 則用于存儲一些額外的信息,比如運行時參數 和 databaseId 等。bindings 類型為 ContextMap,ContextMap 定義在 DynamicContext 中,是一個靜態內部類。該類繼承自 HashMap,并覆寫了 get 方法。它的代碼如下:
static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
// 檢查是否包含 strKey,若包含則直接返回
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
// 從運行時參數中查找結果
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
DynamicContext 對外提供了兩個接口,用于操作 sqlBuilder。分別如下:
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
public String getSql() {
return sqlBuilder.toString().trim();
}
以上就是對 DynamicContext 的簡單介紹,DynamicContext 的源碼不難理解,這里就不多說了。繼續往下分析。
2.2.2.2 解析 SQL 片段
對于一個包含了 ${} 占位符,或 <if>、<where> 等標簽的 SQL,在解析的過程中,會被分解成多個片段。每個片段都有對應的類型,每種類型的片段都有不同的解析邏輯。在源碼中,片段這個概念等價于 sql 節點,即 SqlNode。SqlNode 是一個接口,它有眾多的實現類。其繼承體系如下:

上圖只畫出了部分的實現類,還有一小部分沒畫出來,不過這并不影響接下來的分析。在眾多實現類中,StaticTextSqlNode 用于存儲靜態文本,TextSqlNode 用于存儲帶有 ${} 占位符的文本,IfSqlNode 則用于存儲 <if> 節點的內容。MixedSqlNode 內部維護了一個 SqlNode 集合,用于存儲各種各樣的 SqlNode。接下來,我將會對 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等進行分析,其他的實現類請大家自行分析。Talk is cheap,show you the code.
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 遍歷 SqlNode 集合
for (SqlNode sqlNode : contents) {
// 調用 salNode 對象本身的 apply 方法解析 sql
sqlNode.apply(context);
}
return true;
}
}
MixedSqlNode 可以看做是 SqlNode 實現類對象的容器,凡是實現了 SqlNode 接口的類都可以存儲到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 邏輯比較簡單,即遍歷 SqlNode 集合,并調用其他 SalNode 實現類對象的 apply 方法解析 sql。那下面我們來看看其他 SalNode 實現類的 apply 方法是怎樣實現的。
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
StaticTextSqlNode 用于存儲靜態文本,所以它不需要什么解析邏輯,直接將其存儲的 SQL 片段添加到 DynamicContext 中即可。StaticTextSqlNode 的實現比較簡單,看起來很輕松。下面分析一下 TextSqlNode。
public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;
@Override
public boolean apply(DynamicContext context) {
// 創建 ${} 占位符解析器
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 解析 ${} 占位符,并將解析結果添加到 DynamicContext 中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
// 創建占位符解析器,GenericTokenParser 是一個通用解析器,并非只能解析 ${}
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 通過 ONGL 從用戶傳入的參數中獲取結果
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));
// 通過正則表達式檢測 srtValue 有效性
checkInjection(srtValue);
return srtValue;
}
}
}
如上,GenericTokenParser 是一個通用的標記解析器,用于解析形如 ${xxx},#{xxx} 等標記。GenericTokenParser 負責將標記中的內容抽取出來,并將標記內容交給相應的 TokenHandler 去處理。BindingTokenParser 負責解析標記內容,并將解析結果返回給 GenericTokenParser,用于替換 ${xxx} 標記。舉個例子說明一下吧,如下。
我們有這樣一個 SQL 語句,用于從 article 表中查詢某個作者所寫的文章。如下:
SELECT * FROM article WHERE author = '${author}'
假設我們我們傳入的 author 值為 tianxiaobo,那么該 SQL 最終會被解析成如下的結果:
SELECT * FROM article WHERE author = 'tianxiaobo'
一般情況下,使用 ${author} 接受參數都沒什么問題。但是怕就怕在有人不懷好意,構建了一些惡意的參數。當用這些惡意的參數替換 ${author} 時就會出現災難性問題 -- SQL 注入。比如我們構建這樣一個參數 author = tianxiaobo'; DELETE FROM article;# ,然后我們把這個參數傳給 TextSqlNode 進行解析。得到的結果如下:
SELECT * FROM article WHERE author = 'tianxiaobo'; DELETE FROM article;#'
看到沒,由于傳入的參數沒有經過轉義,最終導致了一條 SQL 被惡意參數拼接成了兩條 SQL。更要命的是,第二天 SQL 會把 article 表的數據清空,這個后果就很嚴重了(從刪庫到跑路)。這就是為什么我們不應該在 SQL 語句中是用 ${} 占位符,風險太大。
分析完 TextSqlNode 的邏輯,接下來,分析 IfSqlNode 的實現。
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// 通過 ONGL 評估 test 表達式的結果
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 若 test 表達式中的條件成立,則調用其他節點的 apply 方法進行解析
contents.apply(context);
return true;
}
return false;
}
}
IfSqlNode 對應的是 <if test='xxx'> 節點,<if> 節點是日常開發中使用頻次比較高的一個節點。它的具體用法我想大家都很熟悉了,這里就不多啰嗦。IfSqlNode 的 apply 方法邏輯并不復雜,首先是通過 ONGL 檢測 test 表達式是否為 true,如果為 true,則調用其他節點的 apply 方法繼續進行解析。需要注意的是 <if> 節點中也可嵌套其他的動態節點,并非只有純文本。因此 contents 變量遍歷指向的是 MixedSqlNode,而非 StaticTextSqlNode。
關于 IfSqlNode 就說到這,接下來分析 WhereSqlNode 的實現。
public class WhereSqlNode extends TrimSqlNode {
/** 前綴列表 */
private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
// 調用父類的構造方法
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
在 MyBatis 中,WhereSqlNode 和 SetSqlNode 都是基于 TrimSqlNode 實現的,所以上面的代碼看起來很簡單。WhereSqlNode 對應于 <where> 節點,關于該節點的用法以及它的應用場景,大家請自行查閱資料。我在分析源碼的過程中,默認大家已經知道了該節點的用途和應用場景。
接下來,我們把目光聚焦在 TrimSqlNode 的實現上。
public class TrimSqlNode implements SqlNode {
private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride;
private final List<String> suffixesToOverride;
private final Configuration configuration;
// 省略構造方法
@Override
public boolean apply(DynamicContext context) {
// 創建具有過濾功能的 DynamicContext
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// 解析節點內容
boolean result = contents.apply(filteredDynamicContext);
// 過濾掉前綴和后綴
filteredDynamicContext.applyAll();
return result;
}
}
如上,apply 方法首選調用了其他 SqlNode 的 apply 方法解析節點內容,這步操作完成后,FilteredDynamicContext 中會得到一條 SQL 片段字符串。接下里需要做的事情是過濾字符串前綴后和后綴,并添加相應的前綴和后綴。這個事情由 FilteredDynamicContext 負責,FilteredDynamicContext 是 TrimSqlNode 的私有內部類。我們去看一下它的代碼。
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
/** 構造方法會將下面兩個布爾值置為 false */
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;
// 省略構造方法
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
// 引用前綴和后綴,也就是對 sql 進行過濾操作,移除掉前綴或后綴
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
// 將當前對象的 sqlBuffer 內容添加到代理類中
delegate.appendSql(sqlBuffer.toString());
}
// 省略部分方法
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
// 設置 prefixApplied 為 true,以下邏輯僅會被執行一次
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
// 檢測當前 sql 字符串是否包含 toRemove 前綴,比如 'AND ', 'AND\t'
if (trimmedUppercaseSql.startsWith(toRemove)) {
// 移除前綴
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// 插入前綴,比如 WHERE
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
// 該方法邏輯與 applyPrefix 大同小異,大家自行分析
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {...}
}
在上面的代碼中,我們重點關注 applyAll 和 applyPrefix 方法,其他的方法大家自行分析。applyAll 方法的邏輯比較簡單,首先從 sqlBuffer 中獲取 SQL 字符串。然后調用 applyPrefix 和 applySuffix 進行過濾操作。最后將過濾后的 SQL 字符串添加到被裝飾的類中。applyPrefix 方法會首先檢測 SQL 字符串是不是以 "AND ","OR ",或 "AND\n", "OR\n" 等前綴開頭,若是則將前綴從 sqlBuffer 中移除。然后將前綴插入到 sqlBuffer 的首部,整個邏輯就結束了。下面寫點代碼簡單驗證一下,如下:
public class SqlNodeTest {
@Test
public void testWhereSqlNode() throws IOException {
String sqlFragment = "AND id = #{id}";
MixedSqlNode msn = new MixedSqlNode(Arrays.asList(new StaticTextSqlNode(sqlFragment)));
WhereSqlNode wsn = new WhereSqlNode(new Configuration(), msn);
DynamicContext dc = new DynamicContext(new Configuration(), new ParamMap<>());
wsn.apply(dc);
System.out.println("解析前:" + sqlFragment);
System.out.println("解析后:" + dc.getSql());
}
}
測試結果如下:

2.2.2.3 解析 #{} 占位符
經過前面的解析,我們已經能從 DynamicContext 獲取到完整的 SQL 語句了。但這并不意味著解析過程就結束了,因為當前的 SQL 語句中還有一種占位符沒有處理,即 #{}。與 ${} 占位符的處理方式不同,MyBatis 并不會直接將 #{} 占位符替換為相應的參數值。#{} 占位符的解析邏輯這里先不多說,等相應的源碼分析完了,答案就明了了。
#{} 占位符的解析邏輯是包含在 SqlSourceBuilder 的 parse 方法中,該方法最終會將解析后的 SQL 以及其他的一些數據封裝到 StaticSqlSource 中。下面,一起來看一下 SqlSourceBuilder 的 parse 方法。
// -☆- SqlSourceBuilder
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 創建 #{} 占位符處理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 創建 #{} 占位符解析器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析 #{} 占位符,并返回解析結果
String sql = parser.parse(originalSql);
// 封裝解析結果到 StaticSqlSource 中,并返回
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
如上,GenericTokenParser 的用途上一節已經介紹過了,就不多說了。接下來,我們重點關注 #{} 占位符處理器 ParameterMappingTokenHandler 的邏輯。
public String handleToken(String content) {
// 獲取 content 的對應的 ParameterMapping
parameterMappings.add(buildParameterMapping(content));
// 返回 ?
return "?";
}
ParameterMappingTokenHandler 的 handleToken 方法看起來比較簡單,但實際上并非如此。GenericTokenParser 負責將 #{} 占位符中的內容抽取出來,并將抽取出的內容傳給 handleToken 方法。handleToken 方法負責將傳入的參數解析成對應的 ParameterMapping 對象,這步操作由 buildParameterMapping 方法完成。下面我們看一下 buildParameterMapping 的源碼。
private ParameterMapping buildParameterMapping(String content) {
/*
* 將 #{xxx} 占位符中的內容解析成 Map。大家可能很好奇一個普通的字符串是怎么解析成 Map 的,
* 舉例說明一下。如下:
*
* #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
*
* 上面占位符中的內容最終會被解析成如下的結果:
*
* {
* "property": "age",
* "typeHandler": "MyTypeHandler",
* "jdbcType": "NUMERIC",
* "javaType": "int"
* }
*
* parseParameterMapping 內部依賴 ParameterExpression 對字符串進行解析,ParameterExpression 的
* 邏輯不是很復雜,這里就不分析了。大家若有興趣,可自行分析
*/
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
// metaParameters 為 DynamicContext 成員變量 bindings 的元信息對象
if (metaParameters.hasGetter(property)) {
propertyType = metaParameters.getGetterType(property);
/*
* parameterType 是運行時參數的類型。如果用戶傳入的是單個參數,比如 Article 對象,此時
* parameterType 為 Article.class。如果用戶傳入的多個參數,比如 [id = 1, author = "coolblog"],
* MyBatis 會使用 ParamMap 封裝這些參數,此時 parameterType 為 ParamMap.class。如果
* parameterType 有相應的 TypeHandler,這里則把 parameterType 設為 propertyType
*/
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
// 如果 property 為空,或 parameterType 是 Map 類型,則將 propertyType 設為 Object.class
propertyType = Object.class;
} else {
/*
* 代碼邏輯走到此分支中,表明 parameterType 是一個自定義的類,
* 比如 Article,此時為該類創建一個元信息對象
*/
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
// 檢測參數對象有沒有與 property 想對應的 getter 方法
if (metaClass.hasGetter(property)) {
// 獲取成員變量的類型
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
// -------------------------- 分割線 ---------------------------
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
// 將 propertyType 賦值給 javaType
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
// 遍歷 propertiesMap
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
// 如果用戶明確配置了 javaType,則以用戶的配置為準
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
// 解析 jdbcType
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {...}
else if ("numericScale".equals(name)) {...}
else if ("resultMap".equals(name)) {...}
else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
}
else if ("jdbcTypeName".equals(name)) {...}
else if ("property".equals(name)) {...}
else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
+ "}. Valid properties are " + parameterProperties);
}
}
if (typeHandlerAlias != null) {
// 解析 TypeHandler
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// 構建 ParameterMapping 對象
return builder.build();
}
如上,buildParameterMapping 代碼很多,邏輯看起來很復雜。但是它做的事情卻不是很多,只有3件事情。如下:
- 解析 content
- 解析 propertyType,對應分割線之上的代碼
- 構建 ParameterMapping 對象,對應分割線之下的代碼
buildParameterMapping 代碼比較多,不太好理解,下面寫個示例演示一下。如下:
public class SqlSourceBuilderTest {
@Test
public void test() {
// 帶有復雜 #{} 占位符的參數,接下里會解析這個占位符
String sql = "SELECT * FROM Author WHERE age = #{age,javaType=int,jdbcType=NUMERIC}";
SqlSourceBuilder sqlSourceBuilder = new SqlSourceBuilder(new Configuration());
SqlSource sqlSource = sqlSourceBuilder.parse(sql, Author.class, new HashMap<>());
BoundSql boundSql = sqlSource.getBoundSql(new Author());
System.out.println(String.format("SQL: %s\n", boundSql.getSql()));
System.out.println(String.format("ParameterMappings: %s", boundSql.getParameterMappings()));
}
}
public class Author {
private Integer id;
private String name;
private Integer age;
// 省略 getter/setter
}
測試結果如下:

正如測試結果所示,SQL 中的 #{age, ...} 占位符被替換成了問號 ?。#{age, ...} 也被解析成了一個 ParameterMapping 對象。
本節的最后,我們再來看一下 StaticSqlSource 的創建過程。如下:
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 創建 BoundSql 對象
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
上面代碼沒有什么太復雜的地方,從上面代碼中可以看出 BoundSql 的創建過程也很簡單。正因為前面經歷了這么復雜的解析邏輯,BoundSql 的創建過程才會如此簡單。到此,關于 BoundSql 構建的過程就分析完了,稍作休息,我們進行后面的分析。
2.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("Unknown statement type: " + ms.getStatementType());
}
}
// 其他方法邏輯均由別的 StatementHandler 代理完成,就不貼代碼了
}
如上,RoutingStatementHandler 的構造方法會根據 MappedStatement 中的 statementType 變量創建不同的 StatementHandler 實現類。默認情況下,statementType 值為 PREPARED。關于 StatementHandler 創建的過程就先分析到這,StatementHandler 創建完成了,后續要做到事情是創建 Statement,以及將運行時參數和 Statement 進行綁定。接下里,就來分析這一塊的邏輯。
2.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;
}
如上,上面代碼的邏輯不復雜,總共包含三個步驟。如下:
- 獲取數據庫連接
- 創建 Statement
- 為 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("Error preparing statement. Cause: " + e, e);
}
}
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 中。關于 TypeHandler 的用途,我在本系列文章的導讀一文介紹過,這里就不贅述了。大家若不熟悉,可以去看看。
2.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, ...}
#{} 占位符解析完畢后,得到的 SQL 如下:
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) // 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.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 方法中針對兩種映射方式進行了處理。一種是嵌套映射,另一種是簡單映射。本文所說的嵌套查詢是指 <ResultMap> 中嵌套了一個 <ResultMap> ,關于此種映射的處理方式本文就不進行分析了。下面我將詳細分析簡單映射的處理邏輯,如下:
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());
}
}
上面方法的邏輯較多,這里簡單總結一下。如下:
- 根據 RowBounds 定位到指定行記錄
- 循環處理多行數據
- 使用鑒別器處理 ResultMap
- 映射 ResultSet,得到映射結果 rowValue
- 存儲結果
在如上幾個步驟中,鑒別器相關的邏輯就不分析了,不是很常用。第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;
}
在上面的方法中,重要的邏輯已經注釋出來了。分別如下:
- 創建實體類對象
- 檢測結果集是否需要自動映射,若需要則進行自動映射
- 按 <resultMap> 中配置的映射關系進行映射
這三處代碼的邏輯比較復雜,接下來按順序進行分節說明。首先分析實體類的創建過程。
2.2.6.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 的重載方法中了,關于該方法,待會再分析。創建完實體類對后,還需要對 <resultMap> 中配置的映射信息進行檢測。若發現有關聯查詢,且關聯查詢結果的加載方式為延遲加載,此時需為實體類生成代理類。舉個例子說明一下,假設有如下兩個實體類:
/** 作者類 */
public class Author {
private Integer id;
private String name;
private Integer age;
private Integer sex;
// 省略 getter/setter
}
/** 文章類 */
public class Article {
private Integer id;
private String title;
// 一對一關系
private Author author;
private String content;
// 省略 getter/setter
}
如上,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("Do not know how to create an instance of " + resultType);
}
如上,createResultObject 方法中包含了4種創建實體類對象的方式。一般情況下,若無特殊要求,MyBatis 會通過 ObjectFactory 調用默認構造方法創建實體類對象。ObjectFactory 是一個接口,大家可以實現這個接口,以按照自己的邏輯控制對象的創建過程。到此,實體類對象已經創建好了,接下里要做的事情是將結果集中的數據映射到實體類對象中。
2.2.6.2 結果集映射
在 MyBatis 中,結果集自動映射有三種等級。三種等級官方文檔上有所說明,這里直接引用一下。如下:
NONE- 禁用自動映射。僅設置手動映射屬性PARTIAL- 將自動映射結果除了那些有內部定義內嵌結果映射的(joins)FULL- 自動映射所有
除了以上三種等級,我們還可以顯示配置 <resultMap> 節點的 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 方法用于檢測是否應為當前結果集應用自動映射。檢測結果取決于 <resultMap> 節點的 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 用于記錄未配置在 <resultMap> 節點中的映射關系。該類定義在 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;
}
上面的代碼有點多,不過不用太擔心,耐心看一下,還是可以看懂的。下面我來總結一下這個方法的邏輯。
- 從 ResultSetWrapper 中獲取未配置在 <resultMap> 中的列名
- 遍歷上一步獲取到的列名列表
- 若列名包含列名前綴,則移除列名前綴,得到屬性名
- 將下劃線形式的列名轉成駝峰式
- 獲取屬性類型
- 獲取類型處理器
- 創建 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);
}
如上,已映射列名與未映射列名的分揀邏輯并不復雜。我簡述一下這個邏輯,首先是從當前數據集中獲取列名集合,然后獲取 <resultMap> 中配置的列名集合。之后遍歷數據集中的列名集合,并判斷列名是否被配置在了 <resultMap> 節點中。若配置了,則表明該列名已有映射關系,此時該列名存入 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 獲取指定指定列的數據,最后將獲取到的數據設置到實體類對象中。到此,基本的結果集映射過程就分析完了。
結果集映射相關的代碼比較多,結果集的映射過程比較復雜的,需要一定的耐心去閱讀和理解代碼。好了,稍作休息,稍后分析關聯查詢相關的邏輯。
2.2.6.3 關聯查詢與延遲加載
我們在學習 MyBatis 框架時,會經常碰到一對一,一對多的使用場景。對于這樣的場景,通常我們可以用一條 SQL 進行多表查詢完成任務。當然我們也可以使用關聯查詢,將一條 SQL 拆成兩條去完成查詢任務。MyBatis 提供了兩個標簽用于支持一對一和一對多的使用場景,分別是 <association> 和 <collection>。下面我來演示一下如何使用 <association> 完成一對一的關聯查詢。先來看看實體類的定義:
/** 作者類 */
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.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 = "mybatis-one-to-one-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;
}
如上,上面對關聯查詢進行了比較多的注釋,導致該方法看起來有點復雜。當然,真實的邏輯確實有點復雜,因為它還調用了其他的很多方法。下面先來總結一下該方法的邏輯:
- 根據 nestedQueryId 獲取 MappedStatement
- 生成參數對象
- 獲取 BoundSql
- 檢測一級緩存中是否有關聯查詢的結果,若有,則將結果設置到實體類對象中
- 若一級緩存未命中,則創建結果加載器 ResultLoader
- 檢測當前屬性是否需要進行延遲加載,若需要,則添加延遲加載相關的對象到 loaderMap 集合中
- 如不需要延遲加載,則直接通過結果加載器加載結果
如上,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("Nested lazy loaded result property '" + property +
"' for query id '" + resultLoader.mappedStatement.getId() +
" already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
}
// 創建 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 testOne2One() {
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 才是。如果大家還記得 2.2.6.1 節中的內容,大概就知道是怎么回事了 - 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 進行查詢。至于查詢結果的抽取過程,并不是本節所關心的點,因此大家自行分析吧。到此,關于關聯查詢與延遲加載就分析完了。最后我們來看一下映射結果的存儲過程是怎樣的。
2.2.6.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 的源碼大家自行分析吧啊,本節就不展開了。
2.3 更新語句的執行過程分析
在上一節中,我較為完整的分析了查詢語句的執行過程。盡管有些地方一筆帶過了,但多數細節都分析到了。如果大家搞懂了查詢語句的執行過程,那么理解更新語句的執行過程也將不在話下。執行更新語句所需處理的情況較之查詢語句要簡單不少,兩者最大的區別更新語句的執行結果類型單一,處理邏輯要簡單不是。除此之外,兩者在緩存的處理上也有比較大的區別。更新過程會立即刷新緩存,而查詢過程則不會。至于其他的不同點,就不一一列舉了。下面開始分析更新語句的執行過程。
2.3.1 更新語句執行過程全貌
首先,我們還是從 MapperMethod 的 execute 方法開始看起。
// -☆- MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: { // 執行插入語句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: { // 執行更新語句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: { // 執行刪除語句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// ...
break;
case FLUSH:
// ...
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {...}
return result;
}
如上,插入、更新以及刪除操作最終都調用了 SqlSession 接口中的方法。這三個方法返回值均是受影響行數,是一個整型值。rowCountResult 方法負責處理這個整型值,該方法的邏輯暫時先不分析,放在最后分析。接下來,我們往下層走一步,進入 SqlSession 實現類 DefaultSqlSession 的代碼中。
// -☆- DefaultSqlSession
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
public int update(String statement, Object parameter) {
try {
dirty = true;
// 獲取 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 調用 Executor 的 update 方法
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
如上,insert 和 delete 方法最終都調用了同一個 update 方法,這就是為什么我把他們歸為一類的原因。既然它們最終調用的都是同一個方法,那么MyBatis 為什么還要在 SqlSession 中提供這么多方法呢,難道只提供 update 方法不行么?答案是:只提供一個 update 方法從實現上完全可行,但是從接口的語義化的角度來說,這樣做并不好。一般情況下,使用者覺得 update 接口方法應該僅負責執行 UPDATE 語句,如果它還兼職執行其他的 SQL 語句,會讓使用者產生疑惑。對于對外的接口,接口功能越單一,語義越清晰越好。在日常開發中,我們為客戶端提供接口時,也應該這樣做。比如我之前寫過一個文章評論的開關接口,我寫的接口如下:
Result openComment();
Result closeComment();
上面接口語義比較清晰,同時沒有參數,后端不用校驗參數,客戶端同學也不用思考傳什么值。如果我像下面這樣定義接口:
Result updateCommentStatus(Integer status); // 0 - 關閉,1 - 開啟
首先這個方法沒有上面兩個方法語義清晰,其次需要傳入一個整型狀態值,客戶端需要注意傳值,后端也要進行校驗。好了,關于接口語義化就先說這么多。扯多了,回歸正題,下面分析 Executor 的 update 方法。如下:
// -☆- CachingExecutor
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 刷新二級緩存
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
// -☆- BaseExecutor
public int update(MappedStatement ms, Object parameter) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 刷新一級緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
如上,Executor 實現類中的方法在進行下一步操作之前,都會先刷新各自的緩存。默認情況下,insert、update 和 delete 操作都會清空一二級緩存。清空緩存的邏輯不復雜,大家自行分析。下面分析 doUpdate 方法,該方法是一個抽象方法,因此我們到 BaseExecutor 的子類 SimpleExecutor 中看看該方法是如何實現的。
// -☆- SimpleExecutor
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 創建 StatementHandler
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 創建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 調用 StatementHandler 的 update 方法
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
StatementHandler 和 Statement 的創建過程前面已經分析過,這里就不重復分析了。下面分析 PreparedStatementHandler 的 update 方法。
// -☆- PreparedStatementHandler
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行 SQL
ps.execute();
// 返回受影響行數
int rows = ps.getUpdateCount();
// 獲取用戶傳入的參數值,參數值類型可能是普通的實體類,也可能是 Map
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
// 獲取自增主鍵的值,并將值填入到參數對象中
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
PreparedStatementHandler 的 update 方法的邏輯比較清晰明了了,更新語句的 SQL 會在此方法中被執行。執行結果為受影響行數,對于 insert 語句,有時候我們還想獲取自增主鍵的值,因此我們需要進行一些額外的操作。這些額外操作的邏輯封裝在 KeyGenerator 的實現類中,下面我們一起看一下 KeyGenerator 的實現邏輯。
2.3.2 KeyGenerator
KeyGenerator 是一個接口,目前它有三個實現類,分別如下:
- Jdbc3KeyGenerator
- SelectKeyGenerator
- NoKeyGenerator
Jdbc3KeyGenerator 用于獲取插入數據后的自增主鍵數值。某些數據庫不支持自增主鍵,需要手動填寫主鍵字段,此時需要借助 SelectKeyGenerator 獲取主鍵值。至于 NoKeyGenerator,這是一個空實現,沒什么可說的。下面,我將分析 Jdbc3KeyGenerator 的源碼,至于 SelectKeyGenerator,大家請自行分析。下面看源碼吧。
// -☆- Jdbc3KeyGenerator
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// 空方法
}
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, getParameters(parameter));
}
public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
ResultSet rs = null;
try {
rs = stmt.getGeneratedKeys();
final Configuration configuration = ms.getConfiguration();
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 獲取主鍵字段
final String[] keyProperties = ms.getKeyProperties();
// 獲取結果集 ResultSet 的元數據
final ResultSetMetaData rsmd = rs.getMetaData();
TypeHandler<?>[] typeHandlers = null;
// ResultSet 中數據的列數要大于等于主鍵的數量
if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
// 遍歷 parameters
for (Object parameter : parameters) {
// 對于批量插入,ResultSet 會返回多行數據
if (!rs.next()) {
break;
}
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (typeHandlers == null) {
// 為每個主鍵屬性獲取 TypeHandler
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
}
// 填充結果到運行時參數中
populateKeys(rs, metaParam, keyProperties, typeHandlers);
}
}
} catch (Exception e) {
throw new ExecutorException(...);
} finally {...}
}
private Collection<Object> getParameters(Object parameter) {
Collection<Object> parameters = null;
if (parameter instanceof Collection) {
parameters = (Collection) parameter;
} else if (parameter instanceof Map) {
Map parameterMap = (Map) parameter;
/*
* 如果 parameter 是 Map 類型,則從其中提取指定 key 對應的值。
* 至于 Map 中為什么會出現 collection/list/array 等鍵。大家
* 可以參考 DefaultSqlSession 的 wrapCollection 方法
*/
if (parameterMap.containsKey("collection")) {
parameters = (Collection) parameterMap.get("collection");
} else if (parameterMap.containsKey("list")) {
parameters = (List) parameterMap.get("list");
} else if (parameterMap.containsKey("array")) {
parameters = Arrays.asList((Object[]) parameterMap.get("array"));
}
}
if (parameters == null) {
parameters = new ArrayList<Object>();
// 將普通的對象添加到 parameters 中
parameters.add(parameter);
}
return parameters;
}
Jdbc3KeyGenerator 的 processBefore 方法是一個空方法,processAfter 則是一個空殼方法,只有一行代碼。Jdbc3KeyGenerator 的重點在 processBatch 方法中,由于存在批量插入的情況,所以該方法的名字類包含 batch 單詞,表示可處理批量插入的結果集。processBatch 方法的邏輯并不是很復雜,主要流程如下:
- 獲取主鍵數組(keyProperties)
- 獲取 ResultSet 元數據
- 遍歷參數列表,為每個主鍵屬性獲取 TypeHandler
- 從 ResultSet 中獲取主鍵數據,并填充到參數中
在上面流程中,第 1~3 步驟都是常規操作,第4個步驟需要分析一下。如下:
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
// 遍歷 keyProperties
for (int i = 0; i < keyProperties.length; i++) {
// 獲取主鍵屬性
String property = keyProperties[i];
TypeHandler<?> th = typeHandlers[i];
if (th != null) {
// 從 ResultSet 中獲取某列的值
Object value = th.getResult(rs, i + 1);
// 設置結果值到運行時參數中
metaParam.setValue(property, value);
}
}
}
如上,populateKeys 方法首先是遍歷主鍵數組,然后通過 TypeHandler 從 ResultSet 中獲取自增主鍵的值,最后再通過元信息對象將自增主鍵的值設置到參數中。
以上就是 Jdbc3KeyGenerator 的原理分析,下面寫個示例演示一下。
本次演示所用到的實體類如下:
public class Author {
private Integer id;
private String name;
private Integer age;
private Integer sex;
private String email;
}
Mapper 接口和映射文件內容如下:
public interface AuthorDao {
int insertMany(List<Author> authors);
}
<insert id="insertMany" keyProperty="id" useGeneratedKeys="true">
INSERT INTO
author (`name`, `age`, `sex`, `email`)
VALUES
<foreach item="author" index="index" collection="list" separator=",">
(#{author.name}, #{author.age}, #{author.sex}, #{author.email})
</foreach>
</insert>
測試代碼如下:
public class InsertManyTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "mybatis-insert-many-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
public void testInsertMany() {
SqlSession session = sqlSessionFactory.openSession();
try {
List<Author> authors = new ArrayList<>();
// 添加多個 Author 對象到 authors 中
authors.add(new Author("tianxiaobo-1", 20, 0, "coolblog.xyz@outlook.com"));
authors.add(new Author("tianxiaobo-2", 18, 0, "coolblog.xyz@outlook.com"));
System.out.println("\nBefore Insert: ");
authors.forEach(author -> System.out.println(" " + author));
System.out.println();
AuthorDao authorDao = session.getMapper(AuthorDao.class);
authorDao.insertMany(authors);
session.commit();
System.out.println("\nAfter Insert: ");
authors.forEach(author -> System.out.println(" " + author));
} finally {
session.close();
}
}
}
在測試代碼中,我創建了一個 Author 集合,并向集合中插入了兩個 Author 對象。然后將集合中的元素批量插入到 author 表中,得到如下結果:

如上圖,執行插入語句前,列表中元素的 id 字段均為 null。插入數據后,列表元素中的 id 字段均被賦值了。好了,到此,關于 Jdbc3KeyGenerator 的原理與使用就分析完了。
2.3.3 處理更新結果
更新語句的執行結果是一個整型值,表示本次更新所影響的行數。由于返回值類型簡單,因此處理邏輯也很簡單。下面我們簡單看一下,放松放松。
// -☆- MapperMethod
private Object rowCountResult(int rowCount) {
final Object result;
/*
* 這里的 method 類型為 MethodSignature,即方法簽名,包含了某個方法較為詳細的信息。
* 某個方法指的是 Mapper 或 Dao 接口中的方法,比如上一節示例 AuthorDao 中的
* insertMany 方法。
*/
if (method.returnsVoid()) {
// 方法返回類型為 void,則不用返回結果,這里將結果置空
result = null;
} else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
// 方法返回類型為 Integer 或 int,直接賦值返回即可
result = rowCount;
} else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
// 如果返回值類型為 Long 或者 long,這里強轉一下即可
result = (long) rowCount;
} else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
// 方法返回類型為布爾類型,若 rowCount > 0,則返回 ture,否則返回 false
result = rowCount > 0;
} else {
throw new BindingException(...);
}
return result;
}
如上,MyBatis 對于更新語句的執行結果處理邏輯足夠簡單,很容易看懂,我就不多說了。
2.4 小節
經過前面前面的分析,相信大家對 MyBatis 執行 SQL 的過程都有比較深入的理解。本章的最后,用一張圖 MyBatis 的執行過程進行一個總結。如下:

在 MyBatis 中,SQL 執行過程的實現代碼是有層次的,每層都有相應的功能。比如,SqlSession 是對外接口的接口,因此它提供了各種語義清晰的方法,供使用者調用。Executor 層做的事情較多,比如一二級緩存功能就是嵌入在該層內的。StatementHandler 層主要是與 JDBC 層面的接口打交道。至于 ParameterHandler 和 ResultSetHandler,一個負責向 SQL 中設置運行時參數,另一個負責處理 SQL 執行結果,它們倆可以看做是 StatementHandler 輔助類。最后看一下右邊橫跨數層的類,Configuration 是一個全局配置類,很多地方都依賴它。MappedStatement 對應 SQL 配置,包含了 SQL 配置的相關信息。BoundSql 中包含了已完成解析的 SQL 語句,以及運行時參數等。
到此,關于 SQL 的執行過程就分析完了。內容比較多,希望大家耐心閱讀。
3. 總結
到這里,本文就接近尾聲了。本篇文章從本月的1號開始寫,一直到16號才寫完初稿。內容之多,完全超出我事先的預計。盡管本文篇幅很大,但仍有部分邏輯和細節沒有分析到,比如 SelectKeyGenerator。對于這些內容,如果大家能耐心看完本文,并且仔細分析了 MyBatis 執行 SQL 的相關源碼,那么對 MyBatis 的原理會有很深的理解。深入理解 MyBatis,對日常工作也會產生積極的影響。比如我現在就以隨心所欲的寫 SQL 映射文件,把不合理的配置統統刪掉。如果遇到 MyBatis 層面的異常,也不用擔心無法解決了。好了,一不小心又扯多了。本篇文章篇幅比較大,這其中可能存在這一些錯誤不妥之處。如果大家發現了,望指明,這里先說聲謝謝。
好了,本文到此就結束了。感謝大家的閱讀。
參考
附錄:MyBatis 源碼分析系列文章列表
| 更新時間 | 標題 |
|---|---|
| 2018-09-11 | MyBatis 源碼分析系列文章合集 |
| 2018-07-16 | MyBatis 源碼分析系列文章導讀 |
| 2018-07-20 | MyBatis 源碼分析 - 配置文件解析過程 |
| 2018-07-30 | MyBatis 源碼分析 - 映射文件解析過程 |
| 2018-08-17 | MyBatis 源碼分析 - SQL 的執行過程 |
| 2018-08-19 | MyBatis 源碼分析 - 內置數據源 |
| 2018-08-25 | MyBatis 源碼分析 - 緩存原理 |
| 2018-08-26 | MyBatis 源碼分析 - 插件機制 |
本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
作者:田小波
本文同步發布在我的個人博客:http://www.tianxiaobo.com

本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

浙公網安備 33010602011771號