MyBatis 源码分析 - SQL 的执行过程
本文速覽
本篇文章較為詳細的介紹了 MyBatis 執行 SQL 的過程。該過程本身比較復雜,牽涉到的技術點比較多。包括但不限于 Mapper 接口代理類的生成、接口方法的解析、SQL 語句的解析、運行時參數的綁定、查詢結果自動映射、延遲加載等。本文對所列舉的技術點,以及部分未列舉的技術點都做了較為詳細的分析。全文篇幅很大,需要大家耐心閱讀。下面來看一下本文的目錄:
源碼分析類文章通常比較枯燥。因此,我在分析源碼的過程中寫了一些示例,同時也繪制了一些圖片。希望通過這些示例和圖片,幫助大家理解 MyBatis 的源碼。
本篇文章篇幅很大,全文字數約 26000 字,閱讀時間預計超過 100 分鐘。通讀本文可能會比較累,大家可以分次閱讀。好了,本文的速覽就先到這,下面進入正文。
?1.簡介
在前面的文章中,我分析了配置文件和映射文件的解析過程。經過前面復雜的解析過程后,現在,MyBatis 已經進入了就緒狀態,等待使用者發號施令。本篇文章我將分析MyBatis 執行 SQL 的過程,該過程比較復雜,涉及的技術點很多。包括但不限于以下技術點:
如果大家能掌握上面的技術點,那么對 MyBatis 的原理將會有很深入的理解。若將以上技術點一一展開分析,會導致文章篇幅很大,因此我打算將以上知識點分成數篇文章進行分析。本篇文章將分析以上列表中的第1個、第2個以及第6個技術點,其他技術點將會在隨后的文章中進行分析。好了,其他的就不多說了,下面開始我們的源碼分析之旅。
?2.SQL 執行過程分析
?2.1 SQL 執行入口分析
在單獨使用 MyBatis 進行數據庫操作時,我們通常都會先調用 SqlSession 接口的 getMapper 方法為我們的 Mapper 接口生成實現類。然后就可以通過 Mapper 進行數據庫操作。比如像下面這樣:
| 1 2 | ArticleMapper articleMapper = session.getMapper(ArticleMapper.class); Article article = articleMapper.findOne(1); |
如果大家對 MyBatis 較為理解,會知道 SqlSession 是通過 JDK 動態代理的方式為接口生成代理對象的。在調用接口方法時,方法調用會被代理邏輯攔截。在代理邏輯中可根據方法名及方法歸屬接口獲取到當前方法對應的 SQL 以及其他一些信息,拿到這些信息即可進行數據庫操作。
上面是一個簡版的 SQL 執行過程,省略了很多細節。下面我們先按照這個簡版的流程進行分析,首先我們來看一下 Mapper 接口的代理對象創建過程。
?2.1.1 為 Mapper 接口創建代理對象
本節,我們從 DefaultSqlSession 的 getMapper 方法開始看起,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // -☆- 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 對應的 MapperProxyFactoryfinal 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 接口生成代理對象了。相關邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // -☆- 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 接口代理對象的創建過程就分析完了?,F在我們的 ArticleMapper 接口指向的代理對象已經創建完畢,下面就可以調用接口方法進行數據庫操作了。由于接口方法會被代理邏輯攔截,所以下面我們把目光聚焦在代理邏輯上面,看看代理邏輯會做哪些事情。
?2.1.2 執行代理邏輯
在 MyBatis 中,Mapper 接口方法的代理邏輯實現的比較簡單。該邏輯首先會對攔截的方法進行一些檢測,以決定是否執行后續的數據庫操作。對應的代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 方法執行 SQLreturn 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 的構造方法,看看它的構造方法中都包含了哪些邏輯。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 | 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 相關的信息,那具體有哪些信息呢?答案在下面的代碼中。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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();// 解析 MappedStatementMappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);// 檢測當前方法是否有對應的 MappedStatementif (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 的構造方法。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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);} } |
上面的代碼用于檢測目標方法的返回類型,以及解析目標方法參數列表。其中,檢測返回類型的目的是為避免查詢方法返回錯誤的類型。比如我們要求接口方法返回一個對象,結果卻返回了對象集合,這會導致類型轉換錯誤。關于返回值類型的解析過程先說到這,下面分析參數列表的解析過程。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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 或 ResultHandlerif (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 的解析邏輯。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class ParamNameResolverTest {@Testpublic 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 私有成員變量 namesObject 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。代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // -☆- 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 方法出現次數比較頻繁,這里分析一下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // -☆- 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, ... paramNfinal 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 方法。如果你不信的話,那我們看源碼吧,源碼面前了無秘密。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // -☆- 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 方法的實現。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // -☆- 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 {// 獲取 MappedStatementMappedStatement 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 方法是怎樣實現的。如下:
| 1 2 3 4 5 6 7 8 9 | // -☆- CachingExecutor public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 獲取 BoundSqlBoundSql boundSql = ms.getBoundSql(parameterObject);// 創建 CacheKeyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 調用重載方法return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } |
上面的代碼用于獲取 BoundSql 對象,創建 CacheKey 對象,然后再將這兩個對象傳給重載方法。關于 BoundSql 的獲取過程較為復雜,我將在下一節進行分析。CacheKey 以及接下來即將出現的一二級緩存將會獨立成文進行分析。
上面的方法和 SimpleExecutor 父類 BaseExecutor 中的實現沒什么區別,有區別的地方在于這個方法所調用的重載方法。我們繼續往下看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // -☆- 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 = nullif (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 方法是如何實現的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // -☆- 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 方法的實現。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // -☆- 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 進行查詢。下面我們繼續進行跟蹤。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // -☆- 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();// 創建 StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 創建 Statementstmt = prepareStatement(handler, ms.getStatementLog());// 執行查詢操作return handler.<E>query(stmt, resultHandler);} finally {// 關閉 StatementcloseStatement(stmt);} } |
上面的方法中仍然有不少的邏輯,完全看不到即將要到達終點的趨勢,不過這離終點又近了一步。接下來,我們先跳過 StatementHandler 和 Statement 創建過程,這兩個對象的創建過程會在后面進行說明。這里,我們以 PreparedStatementHandler 為例,看看它的 query 方法是怎樣實現的。如下:
| 1 2 3 4 5 6 7 8 | // -☆- PreparedStatementHandler public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;// 執行 SQLps.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 類的成員變量信息,如下:
| 1 2 3 4 5 | 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 方法,代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // -☆- MappedStatement public BoundSql getBoundSql(Object parameterObject) {// 調用 sqlSource 的 getBoundSql 獲取 BoundSqlBoundSql 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 也不在話下。好了,下面開始分析。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // -☆- DynamicSqlSource public BoundSql getBoundSql(Object parameterObject) {// 創建 DynamicContextDynamicContext 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 獲取 BoundSqlBoundSql 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 方法的代碼看起來不多,但是邏輯卻并不簡單。該方法由數個步驟組成,這里總結一下:
如上5個步驟中,第5步為常規操作,就不多說了,其他步驟將會在接下來章節中一一進行分析。按照順序,我們先來分析 DynamicContext 的實現。
?2.2.2.1 DynamicContext
DynamicContext 是 SQL 語句構建的上下文,每個 SQL 片段解析完成后,都會將解析結果存入 DynamicContext 中。待所有的 SQL 片段解析完畢后,一條完整的 SQL 語句就會出現在 DynamicContext 對象中。下面我們來看一下 DynamicContext 類的定義。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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) {// 創建 ContextMapif (parameterObject != null && !(parameterObject instanceof Map)) {MetaObject metaObject = configuration.newMetaObject(parameterObject);bindings = new ContextMap(metaObject);} else {bindings = new ContextMap(null);}// 存放運行時參數 parameterObject 以及 databaseIdbindings.put(PARAMETER_OBJECT_KEY, parameterObject);bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());}// 省略部分代碼 } |
如上,上面只貼了 DynamicContext 類的部分代碼。其中 sqlBuilder 變量用于存放 SQL 片段的解析結果,bindings 則用于存儲一些額外的信息,比如運行時參數 和 databaseId 等。bindings 類型為 ContextMap,ContextMap 定義在 DynamicContext 中,是一個靜態內部類。該類繼承自 HashMap,并覆寫了 get 方法。它的代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static class ContextMap extends HashMap<String, Object> {private MetaObject parameterMetaObject;public ContextMap(MetaObject parameterMetaObject) {this.parameterMetaObject = parameterMetaObject;}@Overridepublic 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。分別如下:
| 1 2 3 4 5 6 7 8 | 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.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class MixedSqlNode implements SqlNode {private final List<SqlNode> contents;public MixedSqlNode(List<SqlNode> contents) {this.contents = contents;}@Overridepublic boolean apply(DynamicContext context) {// 遍歷 SqlNode 集合for (SqlNode sqlNode : contents) {// 調用 salNode 對象本身的 apply 方法解析 sqlsqlNode.apply(context);}return true;} } |
MixedSqlNode 可以看做是 SqlNode 實現類對象的容器,凡是實現了 SqlNode 接口的類都可以存儲到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 邏輯比較簡單,即遍歷 SqlNode 集合,并調用其他 SalNode 實現類對象的 apply 方法解析 sql。那下面我們來看看其他 SalNode 實現類的 apply 方法是怎樣實現的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class StaticTextSqlNode implements SqlNode {private final String text;public StaticTextSqlNode(String text) {this.text = text;}@Overridepublic boolean apply(DynamicContext context) {context.appendSql(text);return true;} } |
StaticTextSqlNode 用于存儲靜態文本,所以它不需要什么解析邏輯,直接將其存儲的 SQL 片段添加到 DynamicContext 中即可。StaticTextSqlNode 的實現比較簡單,看起來很輕松。下面分析一下 TextSqlNode。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class TextSqlNode implements SqlNode {private final String text;private final Pattern injectionFilter;@Overridepublic 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;}@Overridepublic 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 表中查詢某個作者所寫的文章。如下:
| 1 | SELECT * FROM article WHERE author = '${author}' |
假設我們我們傳入的 author 值為 tianxiaobo,那么該 SQL 最終會被解析成如下的結果:
| 1 | SELECT * FROM article WHERE author = 'tianxiaobo' |
一般情況下,使用 ${author} 接受參數都沒什么問題。但是怕就怕在有人不懷好意,構建了一些惡意的參數。當用這些惡意的參數替換 ${author} 時就會出現災難性問題 – SQL 注入。比如我們構建這樣一個參數?author = tianxiaobo'; DELETE FROM article;#,然后我們把這個參數傳給 TextSqlNode 進行解析。得到的結果如下:
| 1 | SELECT * FROM article WHERE author = 'tianxiaobo'; DELETE FROM article;#' |
看到沒,由于傳入的參數沒有經過轉義,最終導致了一條 SQL 被惡意參數拼接成了兩條 SQL。更要命的是,第二天 SQL 會把 article 表的數據清空,這個后果就很嚴重了(從刪庫到跑路)。這就是為什么我們不應該在 SQL 語句中是用 ${} 占位符,風險太大。
分析完 TextSqlNode 的邏輯,接下來,分析 IfSqlNode 的實現。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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();}@Overridepublic 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 的實現。
| 1 2 3 4 5 6 7 8 9 10 | 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 的實現上。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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;// 省略構造方法@Overridepublic boolean apply(DynamicContext context) {// 創建具有過濾功能的 DynamicContextFilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);// 解析節點內容boolean result = contents.apply(filteredDynamicContext);// 過濾掉前綴和后綴filteredDynamicContext.applyAll();return result;} } |
如上,apply 方法首選調用了其他 SqlNode 的 apply 方法解析節點內容,這步操作完成后,FilteredDynamicContext 中會得到一條 SQL 片段字符串。接下里需要做的事情是過濾字符串前綴后和后綴,并添加相應的前綴和后綴。這個事情由 FilteredDynamicContext 負責,FilteredDynamicContext 是 TrimSqlNode 的私有內部類。我們去看一下它的代碼。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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;}}}// 插入前綴,比如 WHEREif (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 的首部,整個邏輯就結束了。下面寫點代碼簡單驗證一下,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class SqlNodeTest {@Testpublic 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 方法。
| 1 2 3 4 5 6 7 8 9 10 11 | // -☆- 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 的邏輯。
| 1 2 3 4 5 6 | public String handleToken(String content) {// 獲取 content 的對應的 ParameterMappingparameterMappings.add(buildParameterMapping(content));// 返回 ?return "?"; } |
ParameterMappingTokenHandler 的 handleToken 方法看起來比較簡單,但實際上并非如此。GenericTokenParser 負責將 #{} 占位符中的內容抽取出來,并將抽取出的內容傳給 handleToken 方法。handleToken 放阿飛負責將傳入的參數解析成對應的 ParameterMapping 對象,這步操作由 buildParameterMapping 方法完成。下面我們看一下 buildParameterMapping 的源碼。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 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.classpropertyType = 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 賦值給 javaTypeClass<?> javaType = propertyType;String typeHandlerAlias = null;// 遍歷 propertiesMapfor (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)) {// 解析 jdbcTypebuilder.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) {// 解析 TypeHandlerbuilder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));}// 構建 ParameterMapping 對象return builder.build(); } |
如上,buildParameterMapping 代碼很多,邏輯看起來很復雜。但是它做的事情卻不是很多,只有3件事情。如下:
buildParameterMapping 代碼比較多,不太好理解,下面寫個示例演示一下。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class SqlSourceBuilderTest {@Testpublic 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 的創建過程。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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;}@Overridepublic 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。那它有什么用呢?接下來,我們到代碼中尋找答案。
| 1 2 3 4 5 6 7 8 9 | // -☆- Configuration public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 創建具有路由功能的 StatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 應用插件到 StatementHandler 上statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler; } |
如上,newStatementHandler 方法在創建 StatementHandler 之后,還會應用插件到 StatementHandler 上。關于 MyBatis 的插件機制,后面獨立成文進行講解,這里就不分析了。下面分析一下 RoutingStatementHandler。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 方法中,下面從這個方法開始進行分析。
| 1 2 3 4 5 6 7 8 9 10 11 | // -☆- 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; } |
如上,上面代碼的邏輯不復雜,總共包含三個步驟。如下:
上面三個步驟看起來并不難實現,實際上如果大家愿意寫,也能寫出來。不過 MyBatis 對著三個步驟進行拓展,實現上也相對復雜一下。以獲取數據庫連接為例,MyBatis 并未沒有在 getConnection 方法中直接調用 JDBC DriverManager 的 getConnection 方法獲取獲取連接,而是通過數據源獲取獲取連接。MyBatis 提供了兩種基于 JDBC 接口的數據源,分別為 PooledDataSource 和 UnpooledDataSource。創建或獲取數據庫連接的操作最終是由這兩個數據源執行。限于篇幅問題,本節不打算分析以上兩種數據源的源碼,相關分析會在下一篇文章中展開。
接下來,我將分析 PreparedStatement 的創建,以及 IN 參數設置的過程。按照順序,先來分析 PreparedStatement 的創建過程。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // -☆- PreparedStatementHandler public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {Statement statement = null;try {// 創建 Statementstatement = instantiateStatement(connection);// 設置超時和 FetchSizesetStatementTimeout(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 方法創建 PreparedStatementif (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 中的過程。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // -☆- 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 類型的 parameterMappingif (parameterMapping.getMode() != ParameterMode.OUT) {Object value;// 獲取屬性名String propertyName = parameterMapping.getProperty();// 檢測 BoundSql 的 additionalParameters 是否包含 propertyNameif (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.OTHERjdbcType = 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 語句:
| 1 | SELECT * FROM author WHERE name = #{name} AND age = #{age} |
這個 SQL 語句中包含兩個 #{} 占位符,在運行時這兩個占位符會被解析成兩個 ParameterMapping 對象。如下:
| 1 | ParameterMapping{property='name', mode=IN, javaType=class java.lang.String, jdbcType=null, ...} |
和
| 1 | ParameterMapping{property='age', mode=IN, javaType=class java.lang.Integer, jdbcType=null, ...} |
#{} 占位符解析完畢后,得到的 SQL 如下:
| 1 | SELECT * FROM Author WHERE name = ? AND age = ? |
這里假設下面這個方法與上面的 SQL 對應:
| 1 | Author findByNameAndAge(@Param("name") String name, @Param("age") Integer age) |
該方法的參數列表會被 ParamNameResolver 解析成一個 map,如下:
| 1 2 3 4 | {0: "name",1: "age" } |
假設該方法在運行時有如下的調用:
| 1 | findByNameAndAge("tianxiaobo", 20) // 20歲,好年輕啊,但是回不去了呀 ? |
此時,需要再次借助 ParamNameResolver 力量。這次我們將參數名和運行時的參數值綁定起來,得到如下的映射關系。
| 1 2 3 4 5 6 | {"name": "tianxiaobo","age": 20,"param1": "tianxiaobo","param2": 20 } |
下一步,我們要將運行時參數設置到 SQL 中。由于原 SQL 經過解析后,占位符信息已經被擦除掉了,我們無法直接將運行時參數 SQL 中。不過好在,這些占位符信息被記錄在了 ParameterMapping 中了,MyBatis 會將 ParameterMapping 會按照 #{} 的解析順序存入到 List 中。這樣我們通過 ParameterMapping 在列表中的位置確定它與 SQL 中的哪個???占位符相關聯。同時通過 ParameterMapping 中的 property 字段,我們到“參數名與參數值”映射表中查找具體的參數值。這樣,我們就可以將參數值準確的設置到 SQL 中了,此時 SQL 如下:
| 1 | SELECT * FROM Author WHERE name = "tianxiaobo" AND age = 20 |
整個流程如下圖所示。
當運行時參數被設置到 SQL 中 后,下一步要做的事情是執行 SQL,然后處理 SQL 執行結果。對于更新操作,數據庫一般返回一個 int 行數值,表示受影響行數,這個處理起來比較簡單。但對于查詢操作,返回的結果類型多變,處理方式也很復雜。接下來,我們就來看看 MyBatis 是如何處理查詢結果的。
?2.2.6 處理查詢結果
MyBatis 可以將查詢結果,即結果集 ResultSet 自動映射成實體類對象。這樣使用者就無需再手動操作結果集,并將數據填充到實體類對象中。這可大大降低開發的工作量,提高工作效率。在 MyBatis 中,結果集的處理工作由結果集處理器 ResultSetHandler 執行。ResultSetHandler 是一個接口,它只有一個實現類 DefaultResultSetHandler。結果集的處理入口方法是 handleResultSets,下面來看一下該方法的實現。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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 方法對該結果集進行處理。一般情況下,如果我們不調用存儲過程,不會涉及到多結果集的問題。由于存儲過程并不是很常用,所以關于多結果集的處理邏輯我就不分析了。下面,我們把目光聚焦在單結果集的處理邏輯上。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 方法,該方法用于處理結果集中的數據。下面來看一下這個方法的邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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> ,關于此種映射的處理方式本文就不進行分析了。下面我將詳細分析簡單映射的處理邏輯,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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()) {// 獲取經過鑒別器處理后的 ResultMapResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);// 從 resultSet 中獲取結果Object rowValue = getRowValue(rsw, discriminatedResultMap);// 存儲結果storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());} } |
上面方法的邏輯較多,這里簡單總結一下。如下:
在如上幾個步驟中,鑒別器相關的邏輯就不分析了,不是很常用。第2步的檢測邏輯比較簡單,就不分析了。下面分析第一個步驟對應的代碼邏輯。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 的映射過程,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } |
在上面的方法中,重要的邏輯已經注釋出來了。分別如下:
這三處代碼的邏輯比較復雜,接下來按順序進行分節說明。首先分析實體類的創建過程。
?2.2.6.1 創建實體類對象
在我們的印象里,創建實體類對象是一個很簡單的過程。直接通過 new 關鍵字,或通過反射即可完成任務。大家可能會想,把這么簡單過程也拿出來說說,怕是有湊字數的嫌疑。實則不然,MyBatis 的維護者寫了不少邏輯,以保證能成功創建實體類對象。如果實在無法創建,則拋出異常。下面我們來看一下 MyBatis 創建實體類對象的過程。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // -☆- 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> 中配置的映射信息進行檢測。若發現有關聯查詢,且關聯查詢結果的加載方式為延遲加載,此時需為實體類生成代理類。舉個例子說明一下,假設有如下兩個實體類:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** 作者類 */ 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 重載方法的邏輯,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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> 節點對應的 ResultMappingfinal 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 的自定映射設定。下面,來看一下自動映射相關的邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 如何進行自動映射。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 內部,它的代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 集合的過程,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // -☆- 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 -> authorNamefinal String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());if (property != null && metaObject.hasSetter(property)) {// 檢測當前屬性是否存在于 resultMap 中if (resultMap.getMappedProperties().contains(property)) {continue;}// 獲取屬性對于的類型final Class<?> propertyType = metaObject.getSetterType(property);if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {// 獲取類型處理器final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);// 封裝上面獲取到的信息到 UnMappedColumnAutoMapping 對象中autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));} else {configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property, propertyType);}} else {/** 若 property 為空,或實體類中無 property 屬性,此時無法完成* 列名與實體類屬性建立映射關系。針對這種情況,有三種處理方式,* 1. 什么都不做* 2. 僅打印日志* 3. 拋出異常* 默認情況下,是什么都不做*/configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, (property != null) ? property : propertyName, null);}}// 寫入緩存autoMappingsCache.put(mapKey, autoMapping);}return autoMapping; } |
上面的代碼有點多,不過不用太擔心,耐心看一下,還是可以看懂的。下面我來總結一下這個方法的邏輯。
以上步驟中,除了第一步,其他都是常規操作,無需過多說明。下面來分析第一個步驟的邏輯,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // -☆- 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 是如何將結果集中的數據填充到已映射的實體類字段中的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // -☆- 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;// 獲取 ResultMappingfinal 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> 完成一對一的關聯查詢。先來看看實體類的定義:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** 作者類 */ 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 接口與映射文件的定義。
| 1 2 3 4 | public interface ArticleDao {Article findOne(@Param("id") int id);Author findAuthor(@Param("id") int authorId); } |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <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">SELECTid, author_id, title, content, create_timeFROMarticleWHEREid = #{id}</select><select id="findAuthor" resultType="Author">SELECTid, name, age, sex, emailFROMauthorWHEREid = #{id}</select> </mapper> |
好了,必要在的準備工作做完了,下面可以寫測試代碼了。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class OneToOneTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void prepare() throws IOException {String resource = "mybatis-one-to-one-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);inputStream.close();}@Testpublic 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 方法開始分析,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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 獲取 MappedStatementfinal 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) {// 獲取 BoundSqlfinal 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; } |
如上,上面對關聯查詢進行了比較多的注釋,導致該方法看起來有點復雜。當然,真實的邏輯確實有點復雜,因為它還調用了其他的很多方法。下面先來總結一下該方法的邏輯:
如上,getNestedQueryMappingValue 的中邏輯多是都是和延遲加載有關。除了延遲加載,以上流程中針對一級緩存的檢查是十分有必要的,若緩存命中,可直接取用結果,無需再在執行關聯查詢 SQL。若緩存未命中,接下來就要按部就班執行延遲加載相關邏輯,接下來,分析一下 MyBatis 延遲加載是如何實現的。首先我們來看一下添加延遲加載相關對象到 loaderMap 集合中的邏輯,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 | // -☆- 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> 節點中加入或覆蓋如下配置:
| 1 2 3 4 5 6 | <!-- 開啟延遲加載 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 關閉積極的加載策略 --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 延遲加載的觸發方法 --> <setting name="lazyLoadTriggerMethods" value="equals,hashCode"/> |
上面三個配置 MyBatis 官方文檔中有較為詳細的介紹,大家可以參考官方文檔,我就不詳細介紹了。下面修改一下測試類的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class OneToOneTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void prepare() throws IOException {...}@Testpublic 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 類中,下面一起看一下。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // -☆- 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 兩個變量的值取決于下面的配置。
| 1 2 | <setting name="aggressiveLazyLoading" value="false"/> <setting name="lazyLoadTriggerMethods" value="equals,hashCode"/> |
現在大家知道上面兩個配置是如何在代碼中使用的了,比較簡單,就不多說了。
回到上面的代碼中,如果執行線程未進入第一個條件分支,那么緊接著,代理邏輯會檢查使用者是不是調用了實體類的 setter 方法,如果調用了,就將該屬性對應的 LoadPair 從 loaderMap 中移除。為什么要這么做呢?答案是:使用者既然手動調用 setter 方法,說明使用者想自定義某個屬性的值。此時,延遲加載邏輯不應該再修改該屬性的值,所以這里從 loaderMap 中移除屬性對于的 LoadPair。
最后如果使用者調用的是某個屬性的 getter 方法,且該屬性配置了延遲加載,此時延遲加載邏輯就會被觸發。那接下來,我們來看看延遲加載邏輯是怎樣實現的的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // -☆- ResultLoaderMap public boolean load(String property) throws SQLException {// 從 loaderMap 中移除 property 所對應的 LoadPairLoadPair 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 方法邏輯是怎樣的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 存儲映射結果
存儲映射結果是“查詢結果”處理流程中的最后一環,實際上也是查詢語句執行過程的最后一環。本節內容分析完,整個查詢過程就分析完了,那接下來讓我們帶著喜悅的心情來分析映射結果存儲邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 的實現類。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class DefaultResultContext<T> implements ResultContext<T> {private T resultObject;private int resultCount;/** 狀態字段 */private boolean stopped;// 省略部分代碼@Overridepublic boolean isStopped() {return stopped;}public void nextResultObject(T resultObject) {resultCount++;this.resultObject = resultObject;}@Overridepublic void stop() {this.stopped = true;} } |
如上,DefaultResultContext 中包含了一個狀態字段,表明結果上下文的狀態。在處理多行數據時,MyBatis 會檢查該字段的值,已決定是否需要進行后續的處理。該類的邏輯比較簡單,不多說了。下面再來看一下 DefaultResultHandler 的源碼。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class DefaultResultHandler implements ResultHandler<Object> {private final List<Object> list;public DefaultResultHandler() {list = new ArrayList<Object>();}// 省略部分源碼@Overridepublic 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 方法開始看起。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // -☆- 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 的代碼中。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // -☆- 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;// 獲取 MappedStatementMappedStatement 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 語句,會讓使用者產生疑惑。對于對外的接口,接口功能越單一,語義越清晰越好。在日常開發中,我們為客戶端提供接口時,也應該這樣做。比如我之前寫過一個文章評論的開關接口,我寫的接口如下:
| 1 2 | Result openComment(); Result closeComment(); |
上面接口語義比較清晰,同時沒有參數,后端不用校驗參數,客戶端同學也不用思考傳什么值。如果我像下面這樣定義接口:
| 1 | Result updateCommentStatus(Integer status); // 0 - 關閉,1 - 開啟 |
首先這個方法沒有上面兩個方法語義清晰,其次需要傳入一個整型狀態值,客戶端需要注意傳值,后端也要進行校驗。好了,關于接口語義化就先說這么多。扯多了,回歸正題,下面分析 Executor 的 update 方法。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // -☆- 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 中看看該方法是如何實現的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // -☆- SimpleExecutor public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 創建 StatementHandlerStatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);// 創建 Statementstmt = prepareStatement(handler, ms.getStatementLog());// 調用 StatementHandler 的 update 方法return handler.update(stmt);} finally {closeStatement(stmt);} } |
StatementHandler 和 Statement 的創建過程前面已經分析過,這里就不重復分析了。下面分析 PreparedStatementHandler 的 update 方法。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // -☆- PreparedStatementHandler public int update(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;// 執行 SQLps.execute();// 返回受影響行數int rows = ps.getUpdateCount();// 獲取用戶傳入的參數值,參數值類型可能是普通的實體類,也可能是 MapObject 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,大家請自行分析。下面看源碼吧。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | // -☆- 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) {// 遍歷 parametersfor (Object parameter : parameters) {// 對于批量插入,ResultSet 會返回多行數據if (!rs.next()) {break;}final MetaObject metaParam = configuration.newMetaObject(parameter);if (typeHandlers == null) {// 為每個主鍵屬性獲取 TypeHandlertypeHandlers = 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 方法的邏輯并不是很復雜,主要流程如下:
在上面流程中,第 1~3 步驟都是常規操作,第4個步驟需要分析一下。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {// 遍歷 keyPropertiesfor (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 的原理分析,下面寫個示例演示一下。
本次演示所用到的實體類如下:
| 1 2 3 4 5 6 7 | public class Author {private Integer id;private String name;private Integer age;private Integer sex;private String email; } |
Mapper 接口和映射文件內容如下:
| 1 2 3 | public interface AuthorDao {int insertMany(List<Author> authors); } |
| 1 2 3 4 5 6 7 8 | <insert id="insertMany" keyProperty="id" useGeneratedKeys="true">INSERT INTOauthor (`name`, `age`, `sex`, `email`)VALUES<foreach item="author" index="index" collection="list" separator=",">(#{author.name}, #{author.age}, #{author.sex}, #{author.email})</foreach> </insert> |
測試代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class InsertManyTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void prepare() throws IOException {String resource = "mybatis-insert-many-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);inputStream.close();}@Testpublic 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 處理更新結果
更新語句的執行結果是一個整型值,表示本次更新所影響的行數。由于返回值類型簡單,因此處理邏輯也很簡單。下面我們簡單看一下,放松放松。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // -☆- 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,否則返回 falseresult = 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 技術內幕》- 徐郡明
- MyBatis 官方文檔
?附錄: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 源碼分析 - 插件機制 |
- 本文鏈接:?https://www.tianxiaobo.com/2018/08/17/MyBatis-源碼分析-SQL-的執行過程/
http://www.tianxiaobo.com/2018/08/17/MyBatis-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-SQL-%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B/?
總結
以上是生活随笔為你收集整理的MyBatis 源码分析 - SQL 的执行过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis 源码分析 - 映射文件解
- 下一篇: MyBatis 源码分析 - 内置数据源