<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      MyBatis 源碼分析 - SQL 的執行過程

      * 本文速覽

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

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

      本篇文章篇幅很大,全文字數約 26000 字,閱讀時間預計超過 100 分鐘。通讀本文可能會比較累,大家可以分次閱讀。好了,本文的速覽就先到這,下面進入正文。

      1.簡介

      在前面的文章中,我分析了配置文件和映射文件的解析過程。經過前面復雜的解析過程后,現在,MyBatis 已經進入了就緒狀態,等待使用者發號施令。本篇文章我將分析MyBatis 執行 SQL 的過程,該過程比較復雜,涉及的技術點很多。包括但不限于以下技術點:

      1. 為 mapper 接口生成實現類
      2. 根據配置信息生成 SQL,并將運行時參數設置到 SQL 中
      3. 一二級緩存的實現
      4. 插件機制
      5. 數據庫連接的獲取與管理
      6. 查詢結果的處理,以及延遲加載等

      如果大家能掌握上面的技術點,那么對 MyBatis 的原理將會有很深入的理解。若將以上技術點一一展開分析,會導致文章篇幅很大,因此我打算將以上知識點分成數篇文章進行分析。本篇文章將分析以上列表中的第1個、第2個以及第6個技術點,其他技術點將會在隨后的文章中進行分析。好了,其他的就不多說了,下面開始我們的源碼分析之旅。

      2.SQL 執行過程分析

      2.1 SQL 執行入口分析

      在單獨使用 MyBatis 進行數據庫操作時,我們通常都會先調用 SqlSession 接口的 getMapper 方法為我們的 Mapper 接口生成實現類。然后就可以通過 Mapper 進行數據庫操作。比如像下面這樣:

      ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
      Article article = articleMapper.findOne(1);
      

      如果大家對 MyBatis 較為理解,會知道 SqlSession 是通過 JDK 動態代理的方式為接口生成代理對象的。在調用接口方法時,方法調用會被代理邏輯攔截。在代理邏輯中可根據方法名及方法歸屬接口獲取到當前方法對應的 SQL 以及其他一些信息,拿到這些信息即可進行數據庫操作。

      上面是一個簡版的 SQL 執行過程,省略了很多細節。下面我們先按照這個簡版的流程進行分析,首先我們來看一下 Mapper 接口的代理對象創建過程。

      2.1.1 為 Mapper 接口創建代理對象

      本節,我們從 DefaultSqlSession 的 getMapper 方法開始看起,如下:

      // -☆- DefaultSqlSession
      public <T> T getMapper(Class<T> type) {
          return configuration.<T>getMapper(type, this);
      }
      
      // -☆- Configuration
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
          return mapperRegistry.getMapper(type, sqlSession);
      }
      
      // -☆- MapperRegistry
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
          // 從 knownMappers 中獲取與 type 對應的 MapperProxyFactory
          final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
          if (mapperProxyFactory == null) {
              throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
          }
          try {
              // 創建代理對象
              return mapperProxyFactory.newInstance(sqlSession);
          } catch (Exception e) {
              throw new BindingException("Error getting mapper instance. Cause: " + e, e);
          }
      }
      

      如上,經過連續的調用,Mapper 接口代理對象的創建邏輯初現端倪。如果沒看過我前面的分析文章,大家可能不知道 knownMappers 集合中的元素是何時存入的。這里再說一遍吧,MyBatis 在解析配置文件的 <mappers> 節點的過程中,會調用 MapperRegistry 的 addMapper 方法將 Class 到 MapperProxyFactory 對象的映射關系存入到 knownMappers。具體的代碼就不分析了,大家可以閱讀我之前寫的文章,或者自行分析相關的代碼。

      在獲取到 MapperProxyFactory 對象后,即可調用工廠方法為 Mapper 接口生成代理對象了。相關邏輯如下:

      // -☆- MapperProxyFactory
      public T newInstance(SqlSession sqlSession) {
          /*
           * 創建 MapperProxy 對象,MapperProxy 實現了 
           * InvocationHandler 接口,代理邏輯封裝在此類中
           */
          final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
          return newInstance(mapperProxy);
      }
      
      protected T newInstance(MapperProxy<T> mapperProxy) {
          // 通過 JDK 動態代理創建代理對象
          return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
      }
      

      上面的代碼首先創建了一個 MapperProxy 對象,該對象實現了 InvocationHandler 接口。然后將對象作為參數傳給重載方法,并在重載方法中調用 JDK 動態代理接口為 Mapper 生成代理對象。

      到此,關于 Mapper 接口代理對象的創建過程就分析完了。現在我們的 ArticleMapper 接口指向的代理對象已經創建完畢,下面就可以調用接口方法進行數據庫操作了。由于接口方法會被代理邏輯攔截,所以下面我們把目光聚焦在代理邏輯上面,看看代理邏輯會做哪些事情。

      2.1.2 執行代理邏輯

      在 MyBatis 中,Mapper 接口方法的代理邏輯實現的比較簡單。該邏輯首先會對攔截的方法進行一些檢測,以決定是否執行后續的數據庫操作。對應的代碼如下:

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          try {
              // 如果方法是定義在 Object 類中的,則直接調用
              if (Object.class.equals(method.getDeclaringClass())) {
                  return method.invoke(this, args);
                  
              /*
               * 下面的代碼最早出現在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
               * 新特性 - 默認方法。這段代碼的邏輯就不分析了,有興趣的同學可以
               * 去 Github 上看一下相關的相關的討論(issue #709),鏈接如下:
               * 
               *   https://github.com/mybatis/mybatis-3/issues/709
               */  
              } else if (isDefaultMethod(method)) {
                  return invokeDefaultMethod(proxy, method, args);
              }
          } catch (Throwable t) {
              throw ExceptionUtil.unwrapThrowable(t);
          }
          
          // 從緩存中獲取 MapperMethod 對象,若緩存未命中,則創建 MapperMethod 對象
          final MapperMethod mapperMethod = cachedMapperMethod(method);
          
          // 調用 execute 方法執行 SQL
          return mapperMethod.execute(sqlSession, args);
      }
      

      如上,代理邏輯會首先檢測被攔截的方法是不是定義在 Object 中的,比如 equals、hashCode 方法等。對于這類方法,直接執行即可。除此之外,MyBatis 從 3.4.2 版本開始,對 JDK 1.8 接口的默認方法提供了支持,具體就不分析了。完成相關檢測后,緊接著從緩存中獲取或者創建 MapperMethod 對象,然后通過該對象中的 execute 方法執行 SQL。在分析 execute 方法之前,我們先來看一下 MapperMethod 對象的創建過程。MapperMethod 的創建過程看似普通,但卻包含了一些重要的邏輯,所以不能忽視。

      2.1.2.1 創建 MapperMethod 對象

      本節來分析一下 MapperMethod 的構造方法,看看它的構造方法中都包含了哪些邏輯。如下:

      public class MapperMethod {
      
          private final SqlCommand command;
          private final MethodSignature method;
      
          public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
              // 創建 SqlCommand 對象,該對象包含一些和 SQL 相關的信息
              this.command = new SqlCommand(config, mapperInterface, method);
              // 創建 MethodSignature 對象,從類名中可知,該對象包含了被攔截方法的一些信息
              this.method = new MethodSignature(config, mapperInterface, method);
          }
      }
      

      如上,MapperMethod 構造方法的邏輯很簡單,主要是創建 SqlCommand 和 MethodSignature 對象。這兩個對象分別記錄了不同的信息,這些信息在后續的方法調用中都會被用到。下面我們深入到這兩個類的構造方法中,探索它們的初始化邏輯。

      ① 創建 SqlCommand 對象

      前面說了 SqlCommand 中保存了一些和 SQL 相關的信息,那具體有哪些信息呢?答案在下面的代碼中。

      public static class SqlCommand {
      
          private final String name;
          private final SqlCommandType type;
      
          public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
              final String methodName = method.getName();
              final Class<?> declaringClass = method.getDeclaringClass();
              // 解析 MappedStatement
              MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
              
              // 檢測當前方法是否有對應的 MappedStatement
              if (ms == null) {
                  // 檢測當前方法是否有 @Flush 注解
                  if (method.getAnnotation(Flush.class) != null) {
                      // 設置 name 和 type 遍歷
                      name = null;
                      type = SqlCommandType.FLUSH;
                  } else {
                      /*
                       * 若 ms == null 且方法無 @Flush 注解,此時拋出異常。
                       * 這個異常比較常見,大家應該眼熟吧
                       */ 
                      throw new BindingException("Invalid bound statement (not found): "
                          + mapperInterface.getName() + "." + methodName);
                  }
              } else {
                  // 設置 name 和 type 變量
                  name = ms.getId();
                  type = ms.getSqlCommandType();
                  if (type == SqlCommandType.UNKNOWN) {
                      throw new BindingException("Unknown execution method for: " + name);
                  }
              }
          }
      }
      

      如上,SqlCommand 的構造方法主要用于初始化它的兩個成員變量。代碼不是很長,邏輯也不難理解,就不多說了。繼續往下看。

      ② 創建 MethodSignature 對象

      MethodSignature 即方法簽名,顧名思義,該類保存了一些和目標方法相關的信息。比如目標方法的返回類型,目標方法的參數列表信息等。下面,我們來分析一下 MethodSignature 的構造方法。

      public static class MethodSignature {
      
          private final boolean returnsMany;
          private final boolean returnsMap;
          private final boolean returnsVoid;
          private final boolean returnsCursor;
          private final Class<?> returnType;
          private final String mapKey;
          private final Integer resultHandlerIndex;
          private final Integer rowBoundsIndex;
          private final ParamNameResolver paramNameResolver;
      
          public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      
              // 通過反射解析方法返回類型
              Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
              if (resolvedReturnType instanceof Class<?>) {
                  this.returnType = (Class<?>) resolvedReturnType;
              } else if (resolvedReturnType instanceof ParameterizedType) {
                  this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
              } else {
                  this.returnType = method.getReturnType();
              }
              
              // 檢測返回值類型是否是 void、集合或數組、Cursor、Map 等
              this.returnsVoid = void.class.equals(this.returnType);
              this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
              this.returnsCursor = Cursor.class.equals(this.returnType);
              // 解析 @MapKey 注解,獲取注解內容
              this.mapKey = getMapKey(method);
              this.returnsMap = this.mapKey != null;
              /*
               * 獲取 RowBounds 參數在參數列表中的位置,如果參數列表中
               * 包含多個 RowBounds 參數,此方法會拋出異常
               */ 
              this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
              // 獲取 ResultHandler 參數在參數列表中的位置
              this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
              // 解析參數列表
              this.paramNameResolver = new ParamNameResolver(configuration, method);
          }
      }
      

      上面的代碼用于檢測目標方法的返回類型,以及解析目標方法參數列表。其中,檢測返回類型的目的是為避免查詢方法返回錯誤的類型。比如我們要求接口方法返回一個對象,結果卻返回了對象集合,這會導致類型轉換錯誤。關于返回值類型的解析過程先說到這,下面分析參數列表的解析過程。

      public class ParamNameResolver {
      
          private static final String GENERIC_NAME_PREFIX = "param";
          private final SortedMap<Integer, String> names;
      
          public ParamNameResolver(Configuration config, Method method) {
              // 獲取參數類型列表
              final Class<?>[] paramTypes = method.getParameterTypes();
              // 獲取參數注解
              final Annotation[][] paramAnnotations = method.getParameterAnnotations();
              final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
              int paramCount = paramAnnotations.length;
      
              for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
                  // 檢測當前的參數類型是否為 RowBounds 或 ResultHandler
                  if (isSpecialParameter(paramTypes[paramIndex])) {
                      continue;
                  }
                  String name = null;
                  for (Annotation annotation : paramAnnotations[paramIndex]) {
                      if (annotation instanceof Param) {
                          hasParamAnnotation = true;
                          // 獲取 @Param 注解內容
                          name = ((Param) annotation).value();
                          break;
                      }
                  }
                  // name 為空,表明未給參數配置 @Param 注解
                  if (name == null) {
                      // 檢測是否設置了 useActualParamName 全局配置
                      if (config.isUseActualParamName()) {
                          /*
                           * 通過反射獲取參數名稱。此種方式要求 JDK 版本為 1.8+,
                           * 且要求編譯時加入 -parameters 參數,否則獲取到的參數名
                           * 仍然是 arg1, arg2, ..., argN
                           */
                          name = getActualParamName(method, paramIndex);
                      }
                      if (name == null) {
                          /*
                           * 使用 map.size() 返回值作為名稱,思考一下為什么不這樣寫:
                           *   name = String.valueOf(paramIndex);
                           * 因為如果參數列表中包含 RowBounds 或 ResultHandler,這兩個參數
                           * 會被忽略掉,這樣將導致名稱不連續。
                           *
                           * 比如參數列表 (int p1, int p2, RowBounds rb, int p3)
                           *  - 期望得到名稱列表為 ["0", "1", "2"]
                           *  - 實際得到名稱列表為 ["0", "1", "3"]
                           */
                          name = String.valueOf(map.size());
                      }
                  }
                  // 存儲 paramIndex 到 name 的映射
                  map.put(paramIndex, name);
              }
              names = Collections.unmodifiableSortedMap(map);
          }
      }
      

      以上就是方法參數列表的解析過程,解析完畢后,可得到參數下標到參數名的映射關系,這些映射關系最終存儲在 ParamNameResolver 的 names 成員變量中。這些映射關系將會在后面的代碼中被用到,大家留意一下。

      下面寫點代碼測試一下 ParamNameResolver 的解析邏輯。如下:

      public class ParamNameResolverTest {
      
          @Test
          public void test() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
              Configuration config = new Configuration();
              config.setUseActualParamName(false);
              Method method = ArticleMapper.class.getMethod("select", Integer.class, String.class, RowBounds.class, Article.class);
      
              ParamNameResolver resolver = new ParamNameResolver(config, method);
              Field field = resolver.getClass().getDeclaredField("names");
              field.setAccessible(true);
              // 通過反射獲取 ParamNameResolver 私有成員變量 names
              Object names = field.get(resolver);
      
              System.out.println("names: " + names);
          }
      
          class ArticleMapper {
              public void select(@Param("id") Integer id, @Param("author") String author, RowBounds rb, Article article) {}
          }
      }
      

      測試結果如下:

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

      到此,關于 MapperMethod 的初始化邏輯就分析完了,繼續往下分析。

      2.1.2.2 執行 execute 方法

      前面已經分析了 MapperMethod 的初始化過程,現在 MapperMethod 創建好了。那么,接下來要做的事情是調用 MapperMethod 的 execute 方法,執行 SQL。代碼如下:

      // -☆- MapperMethod
      public Object execute(SqlSession sqlSession, Object[] args) {
          Object result;
          
          // 根據 SQL 類型執行相應的數據庫操作
          switch (command.getType()) {
              case INSERT: {
                  // 對用戶傳入的參數進行轉換,下同
                  Object param = method.convertArgsToSqlCommandParam(args);
                  // 執行插入操作,rowCountResult 方法用于處理返回值
                  result = rowCountResult(sqlSession.insert(command.getName(), param));
                  break;
              }
              case UPDATE: {
                  Object param = method.convertArgsToSqlCommandParam(args);
                  // 執行更新操作
                  result = rowCountResult(sqlSession.update(command.getName(), param));
                  break;
              }
              case DELETE: {
                  Object param = method.convertArgsToSqlCommandParam(args);
                  // 執行刪除操作
                  result = rowCountResult(sqlSession.delete(command.getName(), param));
                  break;
              }
              case SELECT:
                  // 根據目標方法的返回類型進行相應的查詢操作
                  if (method.returnsVoid() && method.hasResultHandler()) {
                      /*
                       * 如果方法返回值為 void,但參數列表中包含 ResultHandler,表明使用者
                       * 想通過 ResultHandler 的方式獲取查詢結果,而非通過返回值獲取結果
                       */
                      executeWithResultHandler(sqlSession, args);
                      result = null;
                  } else if (method.returnsMany()) {
                      // 執行查詢操作,并返回多個結果 
                      result = executeForMany(sqlSession, args);
                  } else if (method.returnsMap()) {
                      // 執行查詢操作,并將結果封裝在 Map 中返回
                      result = executeForMap(sqlSession, args);
                  } else if (method.returnsCursor()) {
                      // 執行查詢操作,并返回一個 Cursor 對象
                      result = executeForCursor(sqlSession, args);
                  } else {
                      Object param = method.convertArgsToSqlCommandParam(args);
                      // 執行查詢操作,并返回一個結果
                      result = sqlSession.selectOne(command.getName(), param);
                  }
                  break;
              case FLUSH:
                  // 執行刷新操作
                  result = sqlSession.flushStatements();
                  break;
              default:
                  throw new BindingException("Unknown execution method for: " + command.getName());
          }
          
          // 如果方法的返回值為基本類型,而返回值卻為 null,此種情況下應拋出異常
          if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
              throw new BindingException("Mapper method '" + command.getName()
                  + " attempted to return null from a method with a primitive return type (" + method.getReturnType()
                  + ").");
          }
          return result;
      }
      

      如上,execute 方法主要由一個 switch 語句組成,用于根據 SQL 類型執行相應的數據庫操作。該方法的邏輯清晰,不需要太多的分析。不過在上面的方法中 convertArgsToSqlCommandParam 方法出現次數比較頻繁,這里分析一下:

      // -☆- MapperMethod
      public Object convertArgsToSqlCommandParam(Object[] args) {
          return paramNameResolver.getNamedParams(args);
      }
      
      public Object getNamedParams(Object[] args) {
          final int paramCount = names.size();
          if (args == null || paramCount == 0) {
              return null;
          } else if (!hasParamAnnotation && paramCount == 1) {
              /*
               * 如果方法參數列表無 @Param 注解,且僅有一個非特別參數,則返回該參數的值。
               * 比如如下方法:
               *     List findList(RowBounds rb, String name)
               * names 如下:
               *     names = {1 : "0"}
               * 此種情況下,返回 args[names.firstKey()],即 args[1] -> name
               */
              return args[names.firstKey()];
          } else {
              final Map<String, Object> param = new ParamMap<Object>();
              int i = 0;
              for (Map.Entry<Integer, String> entry : names.entrySet()) {
                  // 添加 <參數名, 參數值> 鍵值對到 param 中
                  param.put(entry.getValue(), args[entry.getKey()]);
                  // genericParamName = param + index。比如 param1, param2, ... paramN
                  final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
                  /*
                   * 檢測 names 中是否包含 genericParamName,什么情況下會包含?答案如下:
                   *
                   *   使用者顯式將參數名稱配置為 param1,即 @Param("param1")
                   */
                  if (!names.containsValue(genericParamName)) {
                      // 添加 <param*, value> 到 param 中
                      param.put(genericParamName, args[entry.getKey()]);
                  }
                  i++;
              }
              return param;
          }
      }
      

      如上,convertArgsToSqlCommandParam 是一個空殼方法,該方法最終調用了 ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要邏輯是根據條件返回不同的結果,該方法的代碼不是很難理解,我也進行了比較詳細的注釋,就不多說了。

      分析完 convertArgsToSqlCommandParam 的邏輯,接下來說說 MyBatis 對哪些 SQL 指令提供了支持,如下:

      • 查詢語句:SELECT
      • 更新語句:INSERT/UPDATE/DELETE
      • 存儲過程:CALL

      在上面的列表中,我刻意對 SELECT/INSERT/UPDATE/DELETE 等指令進行了分類,分類依據指令的功能以及 MyBatis 執行這些指令的過程。這里把 SELECT 稱為查詢語句,INSERT/UPDATE/DELETE 等稱為更新語句。接下來,先來分析查詢語句的執行過程。

      2.2 查詢語句的執行過程分析

      查詢語句對應的方法比較多,有如下幾種:

      • executeWithResultHandler
      • executeForMany
      • executeForMap
      • executeForCursor

      這些方法在內部調用了 SqlSession 中的一些 select* 方法,比如 selectList、selectMap、selectCursor 等。這些方法的返回值類型是不同的,因此對于每種返回類型,需要有專門的處理方法。以 selectList 方法為例,該方法的返回值類型為 List。但如果我們的 Mapper 或 Dao 的接口方法返回值類型為數組,或者 Set,直接將 List 類型的結果返回給 Mapper/Dao 就不合適了。execute* 等方法只是對 select* 等方法做了一層簡單的封裝,因此接下來我們應該把目光放在這些 select* 方法上。下面我們來分析一下 selectOne 方法的源碼,如下:

      2.2.1 selectOne 方法分析

      本節選擇分析 selectOne 方法,而不是其他的方法,大家或許會覺得奇怪。前面提及了 selectList、selectMap、selectCursor 等方法,這里卻分析一個未提及的方法。這樣做并沒什么特別之處,主要原因是 selectOne 在內部會調用 selectList 方法。這里分析 selectOne 方法是為了告知大家,selectOne 和 selectList 方法是有聯系的,同時分析 selectOne 方法等同于分析 selectList 方法。如果你不信的話,那我們看源碼吧,源碼面前了無秘密。

      // -☆- DefaultSqlSession
      public <T> T selectOne(String statement, Object parameter) {
          // 調用 selectList 獲取結果
          List<T> list = this.<T>selectList(statement, parameter);
          if (list.size() == 1) {
              // 返回結果
              return list.get(0);
          } else if (list.size() > 1) {
              // 如果查詢結果大于1則拋出異常,這個異常也是很常見的
              throw new TooManyResultsException(
                  "Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
          } else {
              return null;
          }
      }
      

      如上,selectOne 方法在內部調用 selectList 了方法,并取 selectList 返回值的第1個元素作為自己的返回值。如果 selectList 返回的列表元素大于1,則拋出異常。上面代碼比較易懂,就不多說了。下面我們來看看 selectList 方法的實現。

      // -☆- DefaultSqlSession
      public <E> List<E> selectList(String statement, Object parameter) {
          // 調用重載方法
          return this.selectList(statement, parameter, RowBounds.DEFAULT);
      }
      
      private final Executor executor;
      
      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
          try {
              // 獲取 MappedStatement
              MappedStatement ms = configuration.getMappedStatement(statement);
              // 調用 Executor 實現類中的 query 方法
              return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
          } catch (Exception e) {
              throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
          } finally {
              ErrorContext.instance().reset();
          }
      }
      

      如上,這里要來說說 executor 變量,該變量類型為 Executor。Executor 是一個接口,它的實現類如下:

      如上,Executor 有這么多的實現類,大家猜一下 executor 變量對應哪個實現類。要弄清楚這個問題,需要大家到源頭去查證。這里提示一下,大家可以跟蹤一下 DefaultSqlSessionFactory 的 openSession 方法,很快就能發現executor 變量創建的蹤跡。限于篇幅原因,本文就不分析 openSession 方法的源碼了。好了,下面我來直接告訴大家 executor 變量對應哪個實現類吧。默認情況下,executor 的類型為 CachingExecutor,該類是一個裝飾器類,用于給目標 Executor 增加二級緩存功能。那目標 Executor 是誰呢?默認情況下是 SimpleExecutor。

      現在大家搞清楚 executor 變量的身份了,接下來繼續分析 selectOne 方法的調用棧。先來看看 CachingExecutor 的 query 方法是怎樣實現的。如下:

      // -☆- CachingExecutor
      public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
          // 獲取 BoundSql
          BoundSql boundSql = ms.getBoundSql(parameterObject);
          // 創建 CacheKey
          CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
          // 調用重載方法
          return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
      

      上面的代碼用于獲取 BoundSql 對象,創建 CacheKey 對象,然后再將這兩個對象傳給重載方法。關于 BoundSql 的獲取過程較為復雜,我將在下一節進行分析。CacheKey 以及接下來即將出現的一二級緩存將會獨立成文進行分析。

      上面的方法和 SimpleExecutor 父類 BaseExecutor 中的實現沒什么區別,有區別的地方在于這個方法所調用的重載方法。我們繼續往下看。

      // -☆- CachingExecutor
      public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          // 從 MappedStatement 中獲取緩存
          Cache cache = ms.getCache();
          // 若映射文件中未配置緩存或參照緩存,此時 cache = null
          if (cache != null) {
              flushCacheIfRequired(ms);
              if (ms.isUseCache() && resultHandler == null) {
                  ensureNoOutParams(ms, boundSql);
                  List<E> list = (List<E>) tcm.getObject(cache, key);
                  if (list == null) {
                      // 若緩存未命中,則調用被裝飾類的 query 方法
                      list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                      tcm.putObject(cache, key, list); // issue #578 and #116
                  }
                  return list;
              }
          }
          // 調用被裝飾類的 query 方法
          return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
      

      上面的代碼涉及到了二級緩存,若二級緩存為空,或未命中,則調用被裝飾類的 query 方法。下面來看一下 BaseExecutor 的中簽名相同的 query 方法是如何實現的。

      // -☆- BaseExecutor
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          if (closed) {
              throw new ExecutorException("Executor was closed.");
          }
          if (queryStack == 0 && ms.isFlushCacheRequired()) {
              clearLocalCache();
          }
          List<E> list;
          try {
              queryStack++;
              // 從一級緩存中獲取緩存項
              list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
              if (list != null) {
                  // 存儲過程相關處理邏輯,本文不分析存儲過程,故該方法不分析了
                  handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
              } else {
                  // 一級緩存未命中,則從數據庫中查詢
                  list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
              }
          } finally {
              queryStack--;
          }
          if (queryStack == 0) {
              // 從一級緩存中延遲加載嵌套查詢結果
              for (DeferredLoad deferredLoad : deferredLoads) {
                  deferredLoad.load();
              }
              deferredLoads.clear();
              if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                  clearLocalCache();
              }
          }
          return list;
      }
      

      如上,上面的方法主要用于從一級緩存中查找查詢結果。若緩存未命中,再向數據庫進行查詢。在上面的代碼中,出現了一個新的類 DeferredLoad,這個類用于延遲加載。該類的實現并不復雜,但是具體用途讓我有點疑惑。這個我目前也未完全搞清楚,就不強行分析了。接下來,我們來看一下 queryFromDatabase 方法的實現。

      // -☆- BaseExecutor
      private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
          ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          List<E> list;
          // 向緩存中存儲一個占位符
          localCache.putObject(key, EXECUTION_PLACEHOLDER);
          try {
              // 調用 doQuery 進行查詢
              list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
          } finally {
              // 移除占位符
              localCache.removeObject(key);
          }
          // 緩存查詢結果
          localCache.putObject(key, list);
          if (ms.getStatementType() == StatementType.CALLABLE) {
              localOutputParameterCache.putObject(key, parameter);
          }
          return list;
      }
      

      上面的代碼仍然不是 selectOne 方法調用棧的終點,拋開緩存操作,queryFromDatabase 最終還會調用 doQuery 進行查詢。下面我們繼續進行跟蹤。

      // -☆- SimpleExecutor
      public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
          Statement stmt = null;
          try {
              Configuration configuration = ms.getConfiguration();
              // 創建 StatementHandler
              StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
              // 創建 Statement
              stmt = prepareStatement(handler, ms.getStatementLog());
              // 執行查詢操作
              return handler.<E>query(stmt, resultHandler);
          } finally {
              // 關閉 Statement
              closeStatement(stmt);
          }
      }
      

      上面的方法中仍然有不少的邏輯,完全看不到即將要到達終點的趨勢,不過這離終點又近了一步。接下來,我們先跳過 StatementHandler 和 Statement 創建過程,這兩個對象的創建過程會在后面進行說明。這里,我們以 PreparedStatementHandler 為例,看看它的 query 方法是怎樣實現的。如下:

      // -☆- PreparedStatementHandler
      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
          PreparedStatement ps = (PreparedStatement) statement;
          // 執行 SQL
          ps.execute();
          // 處理執行結果
          return resultSetHandler.<E>handleResultSets(ps);
      }
      

      到這里似乎看到了希望,整個調用過程總算要結束了。不過先別高興的太早,SQL 執行結果的處理過程也很復雜,稍后將會專門拿出一節內容進行分析。

      以上就是 selectOne 方法的執行過程,盡管我已經簡化了代碼分析,但是整個過程看起來還是很復雜的。查詢過程涉及到了很多方法調用,不把這些調用方法搞清楚,很難對 MyBatis 的查詢過程有深入的理解。所以在接下來的章節中,我將會對一些重要的調用進行分析。如果大家不滿足于泛泛而談,那么接下來咱們一起進行更為深入的探索吧。

      2.2.2 獲取 BoundSql

      我們在執行 SQL 時,一個重要的任務是將 SQL 語句解析出來。我們都知道 SQL 是配置在映射文件中的,但由于映射文件中的 SQL 可能會包含占位符 #{},以及動態 SQL 標簽,比如 <if>、<where> 等。因此,我們并不能直接使用映射文件中配置的 SQL。MyBatis 會將映射文件中的 SQL 解析成一組 SQL 片段。如果某個片段中也包含動態 SQL 相關的標簽,那么,MyBatis 會對該片段再次進行分片。最終,一個 SQL 配置將會被解析成一個 SQL 片段樹。形如下面的圖片:

      我們需要對片段樹進行解析,以便從每個片段對象中獲取相應的內容。然后將這些內容組合起來即可得到一個完成的 SQL 語句,這個完整的 SQL 以及其他的一些信息最終會存儲在 BoundSql 對象中。下面我們來看一下 BoundSql 類的成員變量信息,如下:

      private final String sql;
      private final List<ParameterMapping> parameterMappings;
      private final Object parameterObject;
      private final Map<String, Object> additionalParameters;
      private final MetaObject metaParameters;
      

      下面用一個表格列舉各個成員變量的含義。

      變量名 類型 用途
      sql String 一個完整的 SQL 語句,可能會包含問號 ? 占位符
      parameterMappings List 參數映射列表,SQL 中的每個 #{xxx} 占位符都會被解析成相應的 ParameterMapping 對象
      parameterObject Object 運行時參數,即用戶傳入的參數,比如 Article 對象,或是其他的參數
      additionalParameters Map 附加參數集合,用于存儲一些額外的信息,比如 datebaseId 等
      metaParameters MetaObject additionalParameters 的元信息對象

      以上對 BoundSql 的成員變量做了簡要的說明,部分參數的用途大家現在可能不是很明白。不過不用著急,這些變量在接下來的源碼分析過程中會陸續的出現。到時候對著源碼多思考,或是寫點測試代碼調試一下,即可弄懂。

      好了,現在準備工作已經做好。接下來,開始分析 BoundSql 的構建過程。我們源碼之旅的第一站是 MappedStatement 的 getBoundSql 方法,代碼如下:

      // -☆- MappedStatement
      public BoundSql getBoundSql(Object parameterObject) {
      
          // 調用 sqlSource 的 getBoundSql 獲取 BoundSql
          BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
          List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
          if (parameterMappings == null || parameterMappings.isEmpty()) {
              /*
               * 創建新的 BoundSql,這里的 parameterMap 是 ParameterMap 類型。
               * 由<ParameterMap> 節點進行配置,該節點已經廢棄,不推薦使用。默認情況下,
               * parameterMap.getParameterMappings() 返回空集合
               */ 
              boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
          }
      
          // 省略不重要的邏輯
      
          return boundSql;
      }
      

      如上,MappedStatement 的 getBoundSql 在內部調用了 SqlSource 實現類的 getBoundSql 方法。處理此處的調用,余下的邏輯都不是重要邏輯,就不啰嗦了。接下來,我們把目光轉移到 SqlSource 實現類的 getBoundSql 方法上。SqlSource 是一個接口,它有如下幾個實現類:

      • DynamicSqlSource
      • RawSqlSource
      • StaticSqlSource
      • ProviderSqlSource
      • VelocitySqlSource

      在如上幾個實現類中,我們應該選擇分析哪個實現類的邏輯呢?如果大家分析過 MyBatis 映射文件的解析過程,或者閱讀過我上一篇的關于MyBatis 映射文件分析的文章,那么這個問題不難回答。好了,不賣關子了,我來回答一下這個問題吧。首先我們把最后兩個排除掉,不常用。剩下的三個實現類中,僅前兩個實現類會在映射文件解析的過程中被使用。當 SQL 配置中包含 ${}(不是 #{})占位符,或者包含 <if>、<where> 等標簽時,會被認為是動態 SQL,此時使用 DynamicSqlSource 存儲 SQL 片段。否則,使用 RawSqlSource 存儲 SQL 配置信息。相比之下 DynamicSqlSource 存儲的 SQL 片段類型較多,解析起來也更為復雜一些。因此下面我將分析 DynamicSqlSource 的 getBoundSql 方法。弄懂這個,RawSqlSource 也不在話下。好了,下面開始分析。

      // -☆- DynamicSqlSource
      public BoundSql getBoundSql(Object parameterObject) {
          // 創建 DynamicContext
          DynamicContext context = new DynamicContext(configuration, parameterObject);
      
          // 解析 SQL 片段,并將解析結果存儲到 DynamicContext 中
          rootSqlNode.apply(context);
          
          SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
          Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
          /*
           * 構建 StaticSqlSource,在此過程中將 sql 語句中的占位符 #{} 替換為問號 ?,
           * 并為每個占位符構建相應的 ParameterMapping
           */
          SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
          
          // 調用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
          BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
      
          // 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中
          for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
              boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
          }
          return boundSql;
      }
      

      如上,DynamicSqlSource 的 getBoundSql 方法的代碼看起來不多,但是邏輯卻并不簡單。該方法由數個步驟組成,這里總結一下:

      1. 創建 DynamicContext
      2. 解析 SQL 片段,并將解析結果存儲到 DynamicContext 中
      3. 解析 SQL 語句,并構建 StaticSqlSource
      4. 調用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
      5. 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中

      如上5個步驟中,第5步為常規操作,就不多說了,其他步驟將會在接下來章節中一一進行分析。按照順序,我們先來分析 DynamicContext 的實現。

      2.2.2.1 DynamicContext

      DynamicContext 是 SQL 語句構建的上下文,每個 SQL 片段解析完成后,都會將解析結果存入 DynamicContext 中。待所有的 SQL 片段解析完畢后,一條完整的 SQL 語句就會出現在 DynamicContext 對象中。下面我們來看一下 DynamicContext 類的定義。

      public class DynamicContext {
      
          public static final String PARAMETER_OBJECT_KEY = "_parameter";
          public static final String DATABASE_ID_KEY = "_databaseId";
      
          private final ContextMap bindings;
          private final StringBuilder sqlBuilder = new StringBuilder();
      
          public DynamicContext(Configuration configuration, Object parameterObject) {
              // 創建 ContextMap
              if (parameterObject != null && !(parameterObject instanceof Map)) {
                  MetaObject metaObject = configuration.newMetaObject(parameterObject);
                  bindings = new ContextMap(metaObject);
              } else {
                  bindings = new ContextMap(null);
              }
      
              // 存放運行時參數 parameterObject 以及 databaseId
              bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
              bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
          }
      
          // 省略部分代碼
      }
      

      如上,上面只貼了 DynamicContext 類的部分代碼。其中 sqlBuilder 變量用于存放 SQL 片段的解析結果,bindings 則用于存儲一些額外的信息,比如運行時參數 和 databaseId 等。bindings 類型為 ContextMap,ContextMap 定義在 DynamicContext 中,是一個靜態內部類。該類繼承自 HashMap,并覆寫了 get 方法。它的代碼如下:

      static class ContextMap extends HashMap<String, Object> {
      
          private MetaObject parameterMetaObject;
      
          public ContextMap(MetaObject parameterMetaObject) {
              this.parameterMetaObject = parameterMetaObject;
          }
      
          @Override
          public Object get(Object key) {
              String strKey = (String) key;
              // 檢查是否包含 strKey,若包含則直接返回
              if (super.containsKey(strKey)) {
                  return super.get(strKey);
              }
      
              if (parameterMetaObject != null) {
                  // 從運行時參數中查找結果
                  return parameterMetaObject.getValue(strKey);
              }
      
              return null;
          }
      }
      

      DynamicContext 對外提供了兩個接口,用于操作 sqlBuilder。分別如下:

      public void appendSql(String sql) {
          sqlBuilder.append(sql);
          sqlBuilder.append(" ");
      }
      
      public String getSql() {
          return sqlBuilder.toString().trim();
      }
      

      以上就是對 DynamicContext 的簡單介紹,DynamicContext 的源碼不難理解,這里就不多說了。繼續往下分析。

      2.2.2.2 解析 SQL 片段

      對于一個包含了 ${} 占位符,或 <if>、<where> 等標簽的 SQL,在解析的過程中,會被分解成多個片段。每個片段都有對應的類型,每種類型的片段都有不同的解析邏輯。在源碼中,片段這個概念等價于 sql 節點,即 SqlNode。SqlNode 是一個接口,它有眾多的實現類。其繼承體系如下:

      上圖只畫出了部分的實現類,還有一小部分沒畫出來,不過這并不影響接下來的分析。在眾多實現類中,StaticTextSqlNode 用于存儲靜態文本,TextSqlNode 用于存儲帶有 ${} 占位符的文本,IfSqlNode 則用于存儲 <if> 節點的內容。MixedSqlNode 內部維護了一個 SqlNode 集合,用于存儲各種各樣的 SqlNode。接下來,我將會對 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等進行分析,其他的實現類請大家自行分析。Talk is cheap,show you the code.

      public class MixedSqlNode implements SqlNode {
          private final List<SqlNode> contents;
      
          public MixedSqlNode(List<SqlNode> contents) {
              this.contents = contents;
          }
      
          @Override
          public boolean apply(DynamicContext context) {
              // 遍歷 SqlNode 集合
              for (SqlNode sqlNode : contents) {
                  // 調用 salNode 對象本身的 apply 方法解析 sql
                  sqlNode.apply(context);
              }
              return true;
          }
      }
      

      MixedSqlNode 可以看做是 SqlNode 實現類對象的容器,凡是實現了 SqlNode 接口的類都可以存儲到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 邏輯比較簡單,即遍歷 SqlNode 集合,并調用其他 SalNode 實現類對象的 apply 方法解析 sql。那下面我們來看看其他 SalNode 實現類的 apply 方法是怎樣實現的。

      public class StaticTextSqlNode implements SqlNode {
      
          private final String text;
      
          public StaticTextSqlNode(String text) {
              this.text = text;
          }
      
          @Override
          public boolean apply(DynamicContext context) {
              context.appendSql(text);
              return true;
          }
      }
      

      StaticTextSqlNode 用于存儲靜態文本,所以它不需要什么解析邏輯,直接將其存儲的 SQL 片段添加到 DynamicContext 中即可。StaticTextSqlNode 的實現比較簡單,看起來很輕松。下面分析一下 TextSqlNode。

      public class TextSqlNode implements SqlNode {
      
          private final String text;
          private final Pattern injectionFilter;
      
          @Override
          public boolean apply(DynamicContext context) {
              // 創建 ${} 占位符解析器
              GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
              // 解析 ${} 占位符,并將解析結果添加到 DynamicContext 中
              context.appendSql(parser.parse(text));
              return true;
          }
      
          private GenericTokenParser createParser(TokenHandler handler) {
              // 創建占位符解析器,GenericTokenParser 是一個通用解析器,并非只能解析 ${}
              return new GenericTokenParser("${", "}", handler);
          }
      
          private static class BindingTokenParser implements TokenHandler {
      
              private DynamicContext context;
              private Pattern injectionFilter;
      
              public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
                  this.context = context;
                  this.injectionFilter = injectionFilter;
              }
      
              @Override
              public String handleToken(String content) {
                  Object parameter = context.getBindings().get("_parameter");
                  if (parameter == null) {
                      context.getBindings().put("value", null);
                  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                      context.getBindings().put("value", parameter);
                  }
                  // 通過 ONGL 從用戶傳入的參數中獲取結果
                  Object value = OgnlCache.getValue(content, context.getBindings());
                  String srtValue = (value == null ? "" : String.valueOf(value));
                  // 通過正則表達式檢測 srtValue 有效性
                  checkInjection(srtValue);
                  return srtValue;
              }
          }
      }
      

      如上,GenericTokenParser 是一個通用的標記解析器,用于解析形如 ${xxx},#{xxx} 等標記。GenericTokenParser 負責將標記中的內容抽取出來,并將標記內容交給相應的 TokenHandler 去處理。BindingTokenParser 負責解析標記內容,并將解析結果返回給 GenericTokenParser,用于替換 ${xxx} 標記。舉個例子說明一下吧,如下。

      我們有這樣一個 SQL 語句,用于從 article 表中查詢某個作者所寫的文章。如下:

      SELECT * FROM article WHERE author = '${author}'
      

      假設我們我們傳入的 author 值為 tianxiaobo,那么該 SQL 最終會被解析成如下的結果:

      SELECT * FROM article WHERE author = 'tianxiaobo'
      

      一般情況下,使用 ${author} 接受參數都沒什么問題。但是怕就怕在有人不懷好意,構建了一些惡意的參數。當用這些惡意的參數替換 ${author} 時就會出現災難性問題 -- SQL 注入。比如我們構建這樣一個參數 author = tianxiaobo'; DELETE FROM article;# ,然后我們把這個參數傳給 TextSqlNode 進行解析。得到的結果如下:

      SELECT * FROM article WHERE author = 'tianxiaobo'; DELETE FROM article;#'
      

      看到沒,由于傳入的參數沒有經過轉義,最終導致了一條 SQL 被惡意參數拼接成了兩條 SQL。更要命的是,第二天 SQL 會把 article 表的數據清空,這個后果就很嚴重了(從刪庫到跑路)。這就是為什么我們不應該在 SQL 語句中是用 ${} 占位符,風險太大。

      分析完 TextSqlNode 的邏輯,接下來,分析 IfSqlNode 的實現。

      public class IfSqlNode implements SqlNode {
      
          private final ExpressionEvaluator evaluator;
          private final String test;
          private final SqlNode contents;
      
          public IfSqlNode(SqlNode contents, String test) {
              this.test = test;
              this.contents = contents;
              this.evaluator = new ExpressionEvaluator();
          }
      
          @Override
          public boolean apply(DynamicContext context) {
              // 通過 ONGL 評估 test 表達式的結果
              if (evaluator.evaluateBoolean(test, context.getBindings())) {
                  // 若 test 表達式中的條件成立,則調用其他節點的 apply 方法進行解析
                  contents.apply(context);
                  return true;
              }
              return false;
          }
      }
      

      IfSqlNode 對應的是 <if test='xxx'> 節點,<if> 節點是日常開發中使用頻次比較高的一個節點。它的具體用法我想大家都很熟悉了,這里就不多啰嗦。IfSqlNode 的 apply 方法邏輯并不復雜,首先是通過 ONGL 檢測 test 表達式是否為 true,如果為 true,則調用其他節點的 apply 方法繼續進行解析。需要注意的是 <if> 節點中也可嵌套其他的動態節點,并非只有純文本。因此 contents 變量遍歷指向的是 MixedSqlNode,而非 StaticTextSqlNode。

      關于 IfSqlNode 就說到這,接下來分析 WhereSqlNode 的實現。

      public class WhereSqlNode extends TrimSqlNode {
      
          /** 前綴列表 */
          private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
      
          public WhereSqlNode(Configuration configuration, SqlNode contents) {
              // 調用父類的構造方法
              super(configuration, contents, "WHERE", prefixList, null, null);
          }
      }
      

      在 MyBatis 中,WhereSqlNode 和 SetSqlNode 都是基于 TrimSqlNode 實現的,所以上面的代碼看起來很簡單。WhereSqlNode 對應于 <where> 節點,關于該節點的用法以及它的應用場景,大家請自行查閱資料。我在分析源碼的過程中,默認大家已經知道了該節點的用途和應用場景。

      接下來,我們把目光聚焦在 TrimSqlNode 的實現上。

      public class TrimSqlNode implements SqlNode {
      
          private final SqlNode contents;
          private final String prefix;
          private final String suffix;
          private final List<String> prefixesToOverride;
          private final List<String> suffixesToOverride;
          private final Configuration configuration;
      
         // 省略構造方法
      
          @Override
          public boolean apply(DynamicContext context) {
              // 創建具有過濾功能的 DynamicContext
              FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
              // 解析節點內容
              boolean result = contents.apply(filteredDynamicContext);
              // 過濾掉前綴和后綴
              filteredDynamicContext.applyAll();
              return result;
          }
      }
      

      如上,apply 方法首選調用了其他 SqlNode 的 apply 方法解析節點內容,這步操作完成后,FilteredDynamicContext 中會得到一條 SQL 片段字符串。接下里需要做的事情是過濾字符串前綴后和后綴,并添加相應的前綴和后綴。這個事情由 FilteredDynamicContext 負責,FilteredDynamicContext 是 TrimSqlNode 的私有內部類。我們去看一下它的代碼。

      private class FilteredDynamicContext extends DynamicContext {
      
          private DynamicContext delegate;
          /** 構造方法會將下面兩個布爾值置為 false */
          private boolean prefixApplied;
          private boolean suffixApplied;
          private StringBuilder sqlBuffer;
          
          // 省略構造方法
      
          public void applyAll() {
              sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
              String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
              if (trimmedUppercaseSql.length() > 0) {
                  // 引用前綴和后綴,也就是對 sql 進行過濾操作,移除掉前綴或后綴
                  applyPrefix(sqlBuffer, trimmedUppercaseSql);
                  applySuffix(sqlBuffer, trimmedUppercaseSql);
              }
              // 將當前對象的 sqlBuffer 內容添加到代理類中
              delegate.appendSql(sqlBuffer.toString());
          }
      
          // 省略部分方法
      
          private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
              if (!prefixApplied) {
                  // 設置 prefixApplied 為 true,以下邏輯僅會被執行一次
                  prefixApplied = true;
                  if (prefixesToOverride != null) {
                      for (String toRemove : prefixesToOverride) {
                          // 檢測當前 sql 字符串是否包含 toRemove 前綴,比如 'AND ', 'AND\t'
                          if (trimmedUppercaseSql.startsWith(toRemove)) {
                              // 移除前綴
                              sql.delete(0, toRemove.trim().length());
                              break;
                          }
                      }
                  }
      
                  // 插入前綴,比如 WHERE
                  if (prefix != null) {
                      sql.insert(0, " ");
                      sql.insert(0, prefix);
                  }
              }
          }
      
          // 該方法邏輯與 applyPrefix 大同小異,大家自行分析
          private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {...}
      }
      

      在上面的代碼中,我們重點關注 applyAll 和 applyPrefix 方法,其他的方法大家自行分析。applyAll 方法的邏輯比較簡單,首先從 sqlBuffer 中獲取 SQL 字符串。然后調用 applyPrefix 和 applySuffix 進行過濾操作。最后將過濾后的 SQL 字符串添加到被裝飾的類中。applyPrefix 方法會首先檢測 SQL 字符串是不是以 "AND ","OR ",或 "AND\n", "OR\n" 等前綴開頭,若是則將前綴從 sqlBuffer 中移除。然后將前綴插入到 sqlBuffer 的首部,整個邏輯就結束了。下面寫點代碼簡單驗證一下,如下:

      public class SqlNodeTest {
      
          @Test
          public void testWhereSqlNode() throws IOException {
              String sqlFragment = "AND id = #{id}";
              MixedSqlNode msn = new MixedSqlNode(Arrays.asList(new StaticTextSqlNode(sqlFragment)));
              WhereSqlNode wsn = new WhereSqlNode(new Configuration(), msn);
              DynamicContext dc = new DynamicContext(new Configuration(), new ParamMap<>());
              wsn.apply(dc);
              System.out.println("解析前:" + sqlFragment);
              System.out.println("解析后:" + dc.getSql());
          }
      }
      

      測試結果如下:

      2.2.2.3 解析 #{} 占位符

      經過前面的解析,我們已經能從 DynamicContext 獲取到完整的 SQL 語句了。但這并不意味著解析過程就結束了,因為當前的 SQL 語句中還有一種占位符沒有處理,即 #{}。與 ${} 占位符的處理方式不同,MyBatis 并不會直接將 #{} 占位符替換為相應的參數值。#{} 占位符的解析邏輯這里先不多說,等相應的源碼分析完了,答案就明了了。

      #{} 占位符的解析邏輯是包含在 SqlSourceBuilder 的 parse 方法中,該方法最終會將解析后的 SQL 以及其他的一些數據封裝到 StaticSqlSource 中。下面,一起來看一下 SqlSourceBuilder 的 parse 方法。

      // -☆- SqlSourceBuilder
      public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
          // 創建 #{} 占位符處理器
          ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
          // 創建 #{} 占位符解析器
          GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
          // 解析 #{} 占位符,并返回解析結果
          String sql = parser.parse(originalSql);
          // 封裝解析結果到 StaticSqlSource 中,并返回
          return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
      

      如上,GenericTokenParser 的用途上一節已經介紹過了,就不多說了。接下來,我們重點關注 #{} 占位符處理器 ParameterMappingTokenHandler 的邏輯。

      public String handleToken(String content) {
          // 獲取 content 的對應的 ParameterMapping
          parameterMappings.add(buildParameterMapping(content));
          // 返回 ?
          return "?";
      }
      

      ParameterMappingTokenHandler 的 handleToken 方法看起來比較簡單,但實際上并非如此。GenericTokenParser 負責將 #{} 占位符中的內容抽取出來,并將抽取出的內容傳給 handleToken 方法。handleToken 方法負責將傳入的參數解析成對應的 ParameterMapping 對象,這步操作由 buildParameterMapping 方法完成。下面我們看一下 buildParameterMapping 的源碼。

      private ParameterMapping buildParameterMapping(String content) {
          /*
           * 將 #{xxx} 占位符中的內容解析成 Map。大家可能很好奇一個普通的字符串是怎么解析成 Map 的,
           * 舉例說明一下。如下:
           * 
           *    #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
           *    
           * 上面占位符中的內容最終會被解析成如下的結果:
           * 
           *  {
           *      "property": "age",
           *      "typeHandler": "MyTypeHandler", 
           *      "jdbcType": "NUMERIC", 
           *      "javaType": "int"
           *  }
           * 
           * parseParameterMapping 內部依賴 ParameterExpression 對字符串進行解析,ParameterExpression 的
           * 邏輯不是很復雜,這里就不分析了。大家若有興趣,可自行分析
           */
          Map<String, String> propertiesMap = parseParameterMapping(content);
          String property = propertiesMap.get("property");
          Class<?> propertyType;
          // metaParameters 為 DynamicContext 成員變量 bindings 的元信息對象
          if (metaParameters.hasGetter(property)) {
              propertyType = metaParameters.getGetterType(property);
          
          /*
           * parameterType 是運行時參數的類型。如果用戶傳入的是單個參數,比如 Article 對象,此時 
           * parameterType 為 Article.class。如果用戶傳入的多個參數,比如 [id = 1, author = "coolblog"],
           * MyBatis 會使用 ParamMap 封裝這些參數,此時 parameterType 為 ParamMap.class。如果 
           * parameterType 有相應的 TypeHandler,這里則把 parameterType 設為 propertyType
           */
          } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
              propertyType = parameterType;
          } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
              propertyType = java.sql.ResultSet.class;
          } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
              // 如果 property 為空,或 parameterType 是 Map 類型,則將 propertyType 設為 Object.class
              propertyType = Object.class;
          } else {
              /*
               * 代碼邏輯走到此分支中,表明 parameterType 是一個自定義的類,
               * 比如 Article,此時為該類創建一個元信息對象
               */
              MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
              // 檢測參數對象有沒有與 property 想對應的 getter 方法
              if (metaClass.hasGetter(property)) {
                  // 獲取成員變量的類型
                  propertyType = metaClass.getGetterType(property);
              } else {
                  propertyType = Object.class;
              }
          }
          
          // -------------------------- 分割線 ---------------------------
          
          ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
          
          // 將 propertyType 賦值給 javaType
          Class<?> javaType = propertyType;
          String typeHandlerAlias = null;
          
          // 遍歷 propertiesMap
          for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
              String name = entry.getKey();
              String value = entry.getValue();
              if ("javaType".equals(name)) {
                  // 如果用戶明確配置了 javaType,則以用戶的配置為準
                  javaType = resolveClass(value);
                  builder.javaType(javaType);
              } else if ("jdbcType".equals(name)) {
                  // 解析 jdbcType
                  builder.jdbcType(resolveJdbcType(value));
              } else if ("mode".equals(name)) {...} 
              else if ("numericScale".equals(name)) {...} 
              else if ("resultMap".equals(name)) {...} 
              else if ("typeHandler".equals(name)) {
                  typeHandlerAlias = value;    
              } 
              else if ("jdbcTypeName".equals(name)) {...} 
              else if ("property".equals(name)) {...} 
              else if ("expression".equals(name)) {
                  throw new BuilderException("Expression based parameters are not supported yet");
              } else {
                  throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
                      + "}.  Valid properties are " + parameterProperties);
              }
          }
          if (typeHandlerAlias != null) {
              // 解析 TypeHandler
              builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
          }
          
          // 構建 ParameterMapping 對象
          return builder.build();
      }
      

      如上,buildParameterMapping 代碼很多,邏輯看起來很復雜。但是它做的事情卻不是很多,只有3件事情。如下:

      1. 解析 content
      2. 解析 propertyType,對應分割線之上的代碼
      3. 構建 ParameterMapping 對象,對應分割線之下的代碼

      buildParameterMapping 代碼比較多,不太好理解,下面寫個示例演示一下。如下:

      public class SqlSourceBuilderTest {
      
          @Test
          public void test() {
              // 帶有復雜 #{} 占位符的參數,接下里會解析這個占位符
              String sql = "SELECT * FROM Author WHERE age = #{age,javaType=int,jdbcType=NUMERIC}";
              SqlSourceBuilder sqlSourceBuilder = new SqlSourceBuilder(new Configuration());
              SqlSource sqlSource = sqlSourceBuilder.parse(sql, Author.class, new HashMap<>());
              BoundSql boundSql = sqlSource.getBoundSql(new Author());
              
              System.out.println(String.format("SQL: %s\n", boundSql.getSql()));
              System.out.println(String.format("ParameterMappings: %s", boundSql.getParameterMappings()));
          }
      }
      
      public class Author {
          private Integer id;
          private String name;
          private Integer age;
          
          // 省略 getter/setter
      }
      

      測試結果如下:

      正如測試結果所示,SQL 中的 #{age, ...} 占位符被替換成了問號 ?。#{age, ...} 也被解析成了一個 ParameterMapping 對象。

      本節的最后,我們再來看一下 StaticSqlSource 的創建過程。如下:

      public class StaticSqlSource implements SqlSource {
      
          private final String sql;
          private final List<ParameterMapping> parameterMappings;
          private final Configuration configuration;
      
          public StaticSqlSource(Configuration configuration, String sql) {
              this(configuration, sql, null);
          }
      
          public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
              this.sql = sql;
              this.parameterMappings = parameterMappings;
              this.configuration = configuration;
          }
      
          @Override
          public BoundSql getBoundSql(Object parameterObject) {
              // 創建 BoundSql 對象
              return new BoundSql(configuration, sql, parameterMappings, parameterObject);
          }
      }
      

      上面代碼沒有什么太復雜的地方,從上面代碼中可以看出 BoundSql 的創建過程也很簡單。正因為前面經歷了這么復雜的解析邏輯,BoundSql 的創建過程才會如此簡單。到此,關于 BoundSql 構建的過程就分析完了,稍作休息,我們進行后面的分析。

      2.2.3 創建 StatementHandler

      在 MyBatis 的源碼中,StatementHandler 是一個非常核心接口。之所以說它核心,是因為從代碼分層的角度來說,StatementHandler 是 MyBatis 源碼的邊界,再往下層就是 JDBC 層面的接口了。StatementHandler 需要和 JDBC 層面的接口打交道,它要做的事情有很多。在執行 SQL 之前,StatementHandler 需要創建合適的 Statement 對象,然后填充參數值到 Statement 對象中,最后通過 Statement 對象執行 SQL。這還不算完,待 SQL 執行完畢,還要去處理查詢結果等。這些過程看似簡單,但實現起來卻很復雜。好在,這些過程對應的邏輯并不需要我們親自實現,只需要耐心看一下,難度降低了不少。好了,其他的就不多說了。下面我們來看一下 StatementHandler 的繼承體系。

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

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

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

      public class RoutingStatementHandler implements StatementHandler {
      
          private final StatementHandler delegate;
      
          public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
              ResultHandler resultHandler, BoundSql boundSql) {
      
              // 根據 StatementType 創建不同的 StatementHandler 
              switch (ms.getStatementType()) {
                  case STATEMENT:
                      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                      break;
                  case PREPARED:
                      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                      break;
                  case CALLABLE:
                      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                      break;
                  default:
                      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
              }
          }
          
          // 其他方法邏輯均由別的 StatementHandler 代理完成,就不貼代碼了
      }
      

      如上,RoutingStatementHandler 的構造方法會根據 MappedStatement 中的 statementType 變量創建不同的 StatementHandler 實現類。默認情況下,statementType 值為 PREPARED。關于 StatementHandler 創建的過程就先分析到這,StatementHandler 創建完成了,后續要做到事情是創建 Statement,以及將運行時參數和 Statement 進行綁定。接下里,就來分析這一塊的邏輯。

      2.2.4 設置運行時參數到 SQL 中

      JDBC 提供了三種 Statement 接口,分別是 Statement、PreparedStatement 和 CallableStatement。他們的關系如下:

      上面三個接口的層級分明,其中 Statement 接口提供了執行 SQL,獲取執行結果等基本功能。PreparedStatement 在此基礎上,對 IN 類型的參數提供了支持。使得我們可以使用運行時參數替換 SQL 中的問號 ? 占位符,而不用手動拼接 SQL。CallableStatement 則是 在 PreparedStatement 基礎上,對 OUT 類型的參數提供了支持,該種類型的參數用于保存存儲過程輸出的結果。

      本節,我將分析 PreparedStatement 的創建,以及設置運行時參數到 SQL 中的過程。其他兩種 Statement 的處理過程,大家請自行分析。Statement 的創建入口是在 SimpleExecutor 的 prepareStatement 方法中,下面從這個方法開始進行分析。

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

      如上,上面代碼的邏輯不復雜,總共包含三個步驟。如下:

      1. 獲取數據庫連接
      2. 創建 Statement
      3. 為 Statement 設置 IN 參數

      上面三個步驟看起來并不難實現,實際上如果大家愿意寫,也能寫出來。不過 MyBatis 對著三個步驟進行拓展,實現上也相對復雜一下。以獲取數據庫連接為例,MyBatis 并未沒有在 getConnection 方法中直接調用 JDBC DriverManager 的 getConnection 方法獲取獲取連接,而是通過數據源獲取獲取連接。MyBatis 提供了兩種基于 JDBC 接口的數據源,分別為 PooledDataSource 和 UnpooledDataSource。創建或獲取數據庫連接的操作最終是由這兩個數據源執行。限于篇幅問題,本節不打算分析以上兩種數據源的源碼,相關分析會在下一篇文章中展開。

      接下來,我將分析 PreparedStatement 的創建,以及 IN 參數設置的過程。按照順序,先來分析 PreparedStatement 的創建過程。如下:

      // -☆- PreparedStatementHandler
      public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
          Statement statement = null;
          try {
              // 創建 Statement
              statement = instantiateStatement(connection);
              // 設置超時和 FetchSize
              setStatementTimeout(statement, transactionTimeout);
              setFetchSize(statement);
              return statement;
          } catch (SQLException e) {
              closeStatement(statement);
              throw e;
          } catch (Exception e) {
              closeStatement(statement);
              throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
          }
      }
      
      protected Statement instantiateStatement(Connection connection) throws SQLException {
          String sql = boundSql.getSql();
          // 根據條件調用不同的 prepareStatement 方法創建 PreparedStatement
          if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
              String[] keyColumnNames = mappedStatement.getKeyColumns();
              if (keyColumnNames == null) {
                  return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
              } else {
                  return connection.prepareStatement(sql, keyColumnNames);
              }
          } else if (mappedStatement.getResultSetType() != null) {
              return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
          } else {
              return connection.prepareStatement(sql);
          }
      }
      

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

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

      如上代碼,分割線以上的大段代碼用于獲取 #{xxx} 占位符屬性所對應的運行時參數。分割線以下的代碼則是獲取 #{xxx} 占位符屬性對應的 TypeHandler,并在最后通過 TypeHandler 將運行時參數值設置到 PreparedStatement 中。關于 TypeHandler 的用途,我在本系列文章的導讀一文介紹過,這里就不贅述了。大家若不熟悉,可以去看看。

      2.2.5 #{} 占位符的解析與參數的設置過程梳理

      前面兩節的內容比較多,本節我將對前兩節的部分內容進行梳理,以便大家能夠更好理解這兩節內容之間的聯系。假設我們有這樣一條 SQL 語句:

      SELECT * FROM author WHERE name = #{name} AND age = #{age}
      

      這個 SQL 語句中包含兩個 #{} 占位符,在運行時這兩個占位符會被解析成兩個 ParameterMapping 對象。如下:

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

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

      #{} 占位符解析完畢后,得到的 SQL 如下:

      SELECT * FROM Author WHERE name = ? AND age = ?
      

      這里假設下面這個方法與上面的 SQL 對應:

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

      該方法的參數列表會被 ParamNameResolver 解析成一個 map,如下:

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

      假設該方法在運行時有如下的調用:

      findByNameAndAge("tianxiaobo", 20)    // 20歲,好年輕啊,但是回不去了呀 ??
      

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

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

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

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

      整個流程如下圖所示。

      當運行時參數被設置到 SQL 中 后,下一步要做的事情是執行 SQL,然后處理 SQL 執行結果。對于更新操作,數據庫一般返回一個 int 行數值,表示受影響行數,這個處理起來比較簡單。但對于查詢操作,返回的結果類型多變,處理方式也很復雜。接下來,我們就來看看 MyBatis 是如何處理查詢結果的。

      2.2.6 處理查詢結果

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

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

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

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

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

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

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

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

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

      1. 根據 RowBounds 定位到指定行記錄
      2. 循環處理多行數據
      3. 使用鑒別器處理 ResultMap
      4. 映射 ResultSet,得到映射結果 rowValue
      5. 存儲結果

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

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

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

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

      在上面的方法中,重要的邏輯已經注釋出來了。分別如下:

      1. 創建實體類對象
      2. 檢測結果集是否需要自動映射,若需要則進行自動映射
      3. 按 <resultMap> 中配置的映射關系進行映射

      這三處代碼的邏輯比較復雜,接下來按順序進行分節說明。首先分析實體類的創建過程。

      2.2.6.1 創建實體類對象

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

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

      如上,創建實體類對象的過程被封裝在了 createResultObject 的重載方法中了,關于該方法,待會再分析。創建完實體類對后,還需要對 <resultMap> 中配置的映射信息進行檢測。若發現有關聯查詢,且關聯查詢結果的加載方式為延遲加載,此時需為實體類生成代理類。舉個例子說明一下,假設有如下兩個實體類:

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

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

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

      如上,createResultObject 方法中包含了4種創建實體類對象的方式。一般情況下,若無特殊要求,MyBatis 會通過 ObjectFactory 調用默認構造方法創建實體類對象。ObjectFactory 是一個接口,大家可以實現這個接口,以按照自己的邏輯控制對象的創建過程。到此,實體類對象已經創建好了,接下里要做的事情是將結果集中的數據映射到實體類對象中。

      2.2.6.2 結果集映射

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

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

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

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

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

      private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
      
          // 獲取 UnMappedColumnAutoMapping 列表
          List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
          boolean foundValues = false;
          if (!autoMapping.isEmpty()) {
              for (UnMappedColumnAutoMapping mapping : autoMapping) {
                  // 通過 TypeHandler 從結果集中獲取指定列的數據
                  final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
                  if (value != null) {
                      foundValues = true;
                  }
                  if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
                      // 通過元信息對象設置 value 到實體類對象的指定字段上
                      metaObject.setValue(mapping.property, value);
                  }
              }
          }
          return foundValues;
      }
      

      applyAutomaticMappings 方法的代碼不多,邏輯也不是很復雜。首先是獲取 UnMappedColumnAutoMapping 集合,然后遍歷該集合,并通過 TypeHandler 從結果集中獲取數據,最后再將獲取到的數據設置到實體類對象中。雖然邏輯上看起來沒什么復雜的東西,但如果不清楚 UnMappedColumnAutoMapping 的用途,是無法理解上面代碼的邏輯的。所以下面簡單介紹一下 UnMappedColumnAutoMapping 的用途。

      UnMappedColumnAutoMapping 用于記錄未配置在 <resultMap> 節點中的映射關系。該類定義在 DefaultResultSetHandler 內部,它的代碼如下:

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

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

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

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

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

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

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

      如上,已映射列名與未映射列名的分揀邏輯并不復雜。我簡述一下這個邏輯,首先是從當前數據集中獲取列名集合,然后獲取 <resultMap> 中配置的列名集合。之后遍歷數據集中的列名集合,并判斷列名是否被配置在了 <resultMap> 節點中。若配置了,則表明該列名已有映射關系,此時該列名存入 mappedColumnNames 中。若未配置,則表明列名未與實體類的某個字段形成映射關系,此時該列名存入 unmappedColumnNames 中。這樣,列名的分揀工作就完成了。分揀過程示意圖如下:

      如上圖所示,實體類 Author 的 id 和 name 字段與列名 id 和 name 被配置在了 <resultMap> 中,它們之間形成了映射關系。列名 age、sex 和 email 未配置在 <resultMap> 中,因此未與 Author 中的字段形成映射,所以他們最終都被放入了 unMappedColumnNames 集合中。弄懂了未映射列名獲取的過程,自動映射的代碼邏輯就不難懂了。好了,關于自動映射的分析就先到這,接下來分析一下 MyBatis 是如何將結果集中的數據填充到已映射的實體類字段中的。

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

      如上,applyPropertyMappings 方法首先從 ResultSetWrapper 中獲取已映射列名集合 mappedColumnNames,從 ResultMap 獲取映射對象 ResultMapping 集合。然后遍歷 ResultMapping 集合,再此過程中調用 getPropertyMappingValue 獲取指定指定列的數據,最后將獲取到的數據設置到實體類對象中。到此,基本的結果集映射過程就分析完了。

      結果集映射相關的代碼比較多,結果集的映射過程比較復雜的,需要一定的耐心去閱讀和理解代碼。好了,稍作休息,稍后分析關聯查詢相關的邏輯。

      2.2.6.3 關聯查詢與延遲加載

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

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

      相關表記錄如下:

      接下來看一下 Mapper 接口與映射文件的定義。

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

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

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

      測試結果如下:

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

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

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

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

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

      // -☆- ResultLoaderMap
      public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
          // 將屬性名轉為大寫
          String upperFirst = getUppercaseFirstProperty(property);
          if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
              throw new ExecutorException("Nested lazy loaded result property '" + property +
                                          "' for query id '" + resultLoader.mappedStatement.getId() +
                                          " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
          }
          // 創建 LoadPair,并將 <大寫屬性名,LoadPair對象> 鍵值對添加到 loaderMap 中
          loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
      }
      

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

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

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

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

      測試結果如下:

      從上面結果中可以看出,我們在未調用 getAuthor 方法時,Article 對象中的 author 字段為 null。調用該方法后,再次輸出 Article 對象,發現其 author 字段有值了,表明 author 字段的延遲加載邏輯被觸發了。既然調用 getAuthor 可以觸發延遲加載,那么該方法一定被做過手腳了,不然該方法應該返回 null 才是。如果大家還記得 2.2.6.1 節中的內容,大概就知道是怎么回事了 - MyBatis 會為需要延遲加載的類生成代理類,代理邏輯會攔截實體類的方法調用。默認情況下,MyBatis 會使用 Javassist 為實體類生成代理,代理邏輯封裝在 JavassistProxyFactory 類中,下面一起看一下。

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

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

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

      現在大家知道上面兩個配置是如何在代碼中使用的了,比較簡單,就不多說了。

      回到上面的代碼中,如果執行線程未進入第一個條件分支,那么緊接著,代理邏輯會檢查使用者是不是調用了實體類的 setter 方法,如果調用了,就將該屬性對應的 LoadPair 從 loaderMap 中移除。為什么要這么做呢?答案是:使用者既然手動調用 setter 方法,說明使用者想自定義某個屬性的值。此時,延遲加載邏輯不應該再修改該屬性的值,所以這里從 loaderMap 中移除屬性對于的 LoadPair。

      最后如果使用者調用的是某個屬性的 getter 方法,且該屬性配置了延遲加載,此時延遲加載邏輯就會被觸發。那接下來,我們來看看延遲加載邏輯是怎樣實現的的。

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

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

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

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

      2.2.6.4 存儲映射結果

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

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

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

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

      如上,DefaultResultContext 中包含了一個狀態字段,表明結果上下文的狀態。在處理多行數據時,MyBatis 會檢查該字段的值,已決定是否需要進行后續的處理。該類的邏輯比較簡單,不多說了。下面再來看一下 DefaultResultHandler 的源碼。

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

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

      2.3 更新語句的執行過程分析

      在上一節中,我較為完整的分析了查詢語句的執行過程。盡管有些地方一筆帶過了,但多數細節都分析到了。如果大家搞懂了查詢語句的執行過程,那么理解更新語句的執行過程也將不在話下。執行更新語句所需處理的情況較之查詢語句要簡單不少,兩者最大的區別更新語句的執行結果類型單一,處理邏輯要簡單不是。除此之外,兩者在緩存的處理上也有比較大的區別。更新過程會立即刷新緩存,而查詢過程則不會。至于其他的不同點,就不一一列舉了。下面開始分析更新語句的執行過程。

      2.3.1 更新語句執行過程全貌

      首先,我們還是從 MapperMethod 的 execute 方法開始看起。

      // -☆-  MapperMethod
      public Object execute(SqlSession sqlSession, Object[] args) {
          Object result;
          switch (command.getType()) {
              case INSERT: {    // 執行插入語句
                  Object param = method.convertArgsToSqlCommandParam(args);
                  result = rowCountResult(sqlSession.insert(command.getName(), param));
                  break;
              }
              case UPDATE: {    // 執行更新語句
                  Object param = method.convertArgsToSqlCommandParam(args);
                  result = rowCountResult(sqlSession.update(command.getName(), param));
                  break;
              }
              case DELETE: {    // 執行刪除語句
                  Object param = method.convertArgsToSqlCommandParam(args);
                  result = rowCountResult(sqlSession.delete(command.getName(), param));
                  break;
              }
              case SELECT:
                  // ...
                  break;
              case FLUSH:
                  // ...
                  break;
              default:
                  throw new BindingException("Unknown execution method for: " + command.getName());
          }
          if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {...}
          return result;
      }
      

      如上,插入、更新以及刪除操作最終都調用了 SqlSession 接口中的方法。這三個方法返回值均是受影響行數,是一個整型值。rowCountResult 方法負責處理這個整型值,該方法的邏輯暫時先不分析,放在最后分析。接下來,我們往下層走一步,進入 SqlSession 實現類 DefaultSqlSession 的代碼中。

      // -☆- DefaultSqlSession
      public int insert(String statement, Object parameter) {
          return update(statement, parameter);
      }
      
      public int delete(String statement, Object parameter) {
          return update(statement, parameter);
      }
      
      public int update(String statement, Object parameter) {
          try {
              dirty = true;
              // 獲取 MappedStatement
              MappedStatement ms = configuration.getMappedStatement(statement);
              // 調用 Executor 的 update 方法
              return executor.update(ms, wrapCollection(parameter));
          } catch (Exception e) {
              throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
          } finally {
              ErrorContext.instance().reset();
          }
      }
      

      如上,insert 和 delete 方法最終都調用了同一個 update 方法,這就是為什么我把他們歸為一類的原因。既然它們最終調用的都是同一個方法,那么MyBatis 為什么還要在 SqlSession 中提供這么多方法呢,難道只提供 update 方法不行么?答案是:只提供一個 update 方法從實現上完全可行,但是從接口的語義化的角度來說,這樣做并不好。一般情況下,使用者覺得 update 接口方法應該僅負責執行 UPDATE 語句,如果它還兼職執行其他的 SQL 語句,會讓使用者產生疑惑。對于對外的接口,接口功能越單一,語義越清晰越好。在日常開發中,我們為客戶端提供接口時,也應該這樣做。比如我之前寫過一個文章評論的開關接口,我寫的接口如下:

      Result openComment();
      Result closeComment();
      

      上面接口語義比較清晰,同時沒有參數,后端不用校驗參數,客戶端同學也不用思考傳什么值。如果我像下面這樣定義接口:

      Result updateCommentStatus(Integer status);    // 0 - 關閉,1 - 開啟
      

      首先這個方法沒有上面兩個方法語義清晰,其次需要傳入一個整型狀態值,客戶端需要注意傳值,后端也要進行校驗。好了,關于接口語義化就先說這么多。扯多了,回歸正題,下面分析 Executor 的 update 方法。如下:

      // -☆- CachingExecutor
      public int update(MappedStatement ms, Object parameterObject) throws SQLException {
          // 刷新二級緩存
          flushCacheIfRequired(ms);
          return delegate.update(ms, parameterObject);
      }
      
      // -☆- BaseExecutor
      public int update(MappedStatement ms, Object parameter) throws SQLException {
          if (closed) {
              throw new ExecutorException("Executor was closed.");
          }
          // 刷新一級緩存
          clearLocalCache();
          return doUpdate(ms, parameter);
      }
      

      如上,Executor 實現類中的方法在進行下一步操作之前,都會先刷新各自的緩存。默認情況下,insert、update 和 delete 操作都會清空一二級緩存。清空緩存的邏輯不復雜,大家自行分析。下面分析 doUpdate 方法,該方法是一個抽象方法,因此我們到 BaseExecutor 的子類 SimpleExecutor 中看看該方法是如何實現的。

      // -☆- SimpleExecutor
      public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
          Statement stmt = null;
          try {
              Configuration configuration = ms.getConfiguration();
              // 創建 StatementHandler
              StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
              // 創建 Statement
              stmt = prepareStatement(handler, ms.getStatementLog());
              // 調用 StatementHandler 的 update 方法
              return handler.update(stmt);
          } finally {
              closeStatement(stmt);
          }
      }
      

      StatementHandler 和 Statement 的創建過程前面已經分析過,這里就不重復分析了。下面分析 PreparedStatementHandler 的 update 方法。

      // -☆- PreparedStatementHandler
      public int update(Statement statement) throws SQLException {
          PreparedStatement ps = (PreparedStatement) statement;
          // 執行 SQL
          ps.execute();
          // 返回受影響行數
          int rows = ps.getUpdateCount();
          // 獲取用戶傳入的參數值,參數值類型可能是普通的實體類,也可能是 Map
          Object parameterObject = boundSql.getParameterObject();
          
          KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
          // 獲取自增主鍵的值,并將值填入到參數對象中
          keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
          return rows;
      }
      

      PreparedStatementHandler 的 update 方法的邏輯比較清晰明了了,更新語句的 SQL 會在此方法中被執行。執行結果為受影響行數,對于 insert 語句,有時候我們還想獲取自增主鍵的值,因此我們需要進行一些額外的操作。這些額外操作的邏輯封裝在 KeyGenerator 的實現類中,下面我們一起看一下 KeyGenerator 的實現邏輯。

      2.3.2 KeyGenerator

      KeyGenerator 是一個接口,目前它有三個實現類,分別如下:

      1. Jdbc3KeyGenerator
      2. SelectKeyGenerator
      3. NoKeyGenerator

      Jdbc3KeyGenerator 用于獲取插入數據后的自增主鍵數值。某些數據庫不支持自增主鍵,需要手動填寫主鍵字段,此時需要借助 SelectKeyGenerator 獲取主鍵值。至于 NoKeyGenerator,這是一個空實現,沒什么可說的。下面,我將分析 Jdbc3KeyGenerator 的源碼,至于 SelectKeyGenerator,大家請自行分析。下面看源碼吧。

      // -☆- Jdbc3KeyGenerator
      public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
          // 空方法
      }
      
      public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
          processBatch(ms, stmt, getParameters(parameter));
      }
      
      public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
          ResultSet rs = null;
          try {
              rs = stmt.getGeneratedKeys();
              final Configuration configuration = ms.getConfiguration();
              final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
              // 獲取主鍵字段
              final String[] keyProperties = ms.getKeyProperties();
              // 獲取結果集 ResultSet 的元數據
              final ResultSetMetaData rsmd = rs.getMetaData();
              TypeHandler<?>[] typeHandlers = null;
              // ResultSet 中數據的列數要大于等于主鍵的數量
              if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
                  // 遍歷 parameters
                  for (Object parameter : parameters) {
                      // 對于批量插入,ResultSet 會返回多行數據
                      if (!rs.next()) {
                          break;
                      }
                      final MetaObject metaParam = configuration.newMetaObject(parameter);
                      if (typeHandlers == null) {
                          // 為每個主鍵屬性獲取 TypeHandler
                          typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
                      }
      
                      // 填充結果到運行時參數中
                      populateKeys(rs, metaParam, keyProperties, typeHandlers);
                  }
              }
          } catch (Exception e) {
              throw new ExecutorException(...);
          } finally {...}
      }
      
      private Collection<Object> getParameters(Object parameter) {
          Collection<Object> parameters = null;
          if (parameter instanceof Collection) {
              parameters = (Collection) parameter;
          } else if (parameter instanceof Map) {
              Map parameterMap = (Map) parameter;
              /*
               * 如果 parameter 是 Map 類型,則從其中提取指定 key 對應的值。
               * 至于 Map 中為什么會出現 collection/list/array 等鍵。大家
               * 可以參考 DefaultSqlSession 的 wrapCollection 方法
               */
              if (parameterMap.containsKey("collection")) {
                  parameters = (Collection) parameterMap.get("collection");
              } else if (parameterMap.containsKey("list")) {
                  parameters = (List) parameterMap.get("list");
              } else if (parameterMap.containsKey("array")) {
                  parameters = Arrays.asList((Object[]) parameterMap.get("array"));
              }
          }
          if (parameters == null) {
              parameters = new ArrayList<Object>();
              // 將普通的對象添加到 parameters 中
              parameters.add(parameter);
          }
          return parameters;
      }
      

      Jdbc3KeyGenerator 的 processBefore 方法是一個空方法,processAfter 則是一個空殼方法,只有一行代碼。Jdbc3KeyGenerator 的重點在 processBatch 方法中,由于存在批量插入的情況,所以該方法的名字類包含 batch 單詞,表示可處理批量插入的結果集。processBatch 方法的邏輯并不是很復雜,主要流程如下:

      1. 獲取主鍵數組(keyProperties)
      2. 獲取 ResultSet 元數據
      3. 遍歷參數列表,為每個主鍵屬性獲取 TypeHandler
      4. 從 ResultSet 中獲取主鍵數據,并填充到參數中

      在上面流程中,第 1~3 步驟都是常規操作,第4個步驟需要分析一下。如下:

      private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
          // 遍歷 keyProperties
          for (int i = 0; i < keyProperties.length; i++) {
              // 獲取主鍵屬性
              String property = keyProperties[i];
              TypeHandler<?> th = typeHandlers[i];
              if (th != null) {
                  // 從 ResultSet 中獲取某列的值
                  Object value = th.getResult(rs, i + 1);
                  // 設置結果值到運行時參數中
                  metaParam.setValue(property, value);
              }
          }
      }
      

      如上,populateKeys 方法首先是遍歷主鍵數組,然后通過 TypeHandler 從 ResultSet 中獲取自增主鍵的值,最后再通過元信息對象將自增主鍵的值設置到參數中。

      以上就是 Jdbc3KeyGenerator 的原理分析,下面寫個示例演示一下。

      本次演示所用到的實體類如下:

      public class Author {
          private Integer id;
          private String name;
          private Integer age;
          private Integer sex;
          private String email;
      }
      

      Mapper 接口和映射文件內容如下:

      public interface AuthorDao {
          int insertMany(List<Author> authors);
      }
      
      <insert id="insertMany" keyProperty="id" useGeneratedKeys="true">
          INSERT INTO
              author (`name`, `age`, `sex`, `email`)
          VALUES
          <foreach item="author" index="index" collection="list" separator=",">
              (#{author.name}, #{author.age}, #{author.sex}, #{author.email})
          </foreach>
      </insert>
      

      測試代碼如下:

      public class InsertManyTest {
      
          private SqlSessionFactory sqlSessionFactory;
      
          @Before
          public void prepare() throws IOException {
              String resource = "mybatis-insert-many-config.xml";
              InputStream inputStream = Resources.getResourceAsStream(resource);
              sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
              inputStream.close();
          }
      
          @Test
          public void testInsertMany() {
              SqlSession session = sqlSessionFactory.openSession();
              try {
                  List<Author> authors = new ArrayList<>();
                  // 添加多個 Author 對象到 authors 中
                  authors.add(new Author("tianxiaobo-1", 20, 0, "coolblog.xyz@outlook.com"));
                  authors.add(new Author("tianxiaobo-2", 18, 0, "coolblog.xyz@outlook.com"));
      
                  System.out.println("\nBefore Insert: ");
                  authors.forEach(author -> System.out.println("  " + author));
                  System.out.println();
      
                  AuthorDao authorDao = session.getMapper(AuthorDao.class);
                  authorDao.insertMany(authors);
                  session.commit();
      
                  System.out.println("\nAfter Insert: ");
                  authors.forEach(author -> System.out.println("  " + author));
              } finally {
                  session.close();
              }
          }
      }
      

      在測試代碼中,我創建了一個 Author 集合,并向集合中插入了兩個 Author 對象。然后將集合中的元素批量插入到 author 表中,得到如下結果:

      如上圖,執行插入語句前,列表中元素的 id 字段均為 null。插入數據后,列表元素中的 id 字段均被賦值了。好了,到此,關于 Jdbc3KeyGenerator 的原理與使用就分析完了。

      2.3.3 處理更新結果

      更新語句的執行結果是一個整型值,表示本次更新所影響的行數。由于返回值類型簡單,因此處理邏輯也很簡單。下面我們簡單看一下,放松放松。

      // -☆-  MapperMethod
      private Object rowCountResult(int rowCount) {
          final Object result;
      
          /*
           * 這里的 method 類型為 MethodSignature,即方法簽名,包含了某個方法較為詳細的信息。
           * 某個方法指的是 Mapper 或 Dao 接口中的方法,比如上一節示例 AuthorDao 中的
           * insertMany 方法。
           */
          if (method.returnsVoid()) {
              // 方法返回類型為 void,則不用返回結果,這里將結果置空
              result = null;
          } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
              // 方法返回類型為 Integer 或 int,直接賦值返回即可
              result = rowCount;
          } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
              // 如果返回值類型為 Long 或者 long,這里強轉一下即可
              result = (long) rowCount;
          } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
              // 方法返回類型為布爾類型,若 rowCount > 0,則返回 ture,否則返回 false
              result = rowCount > 0;
          } else {
              throw new BindingException(...);
          }
          return result;
      }
      

      如上,MyBatis 對于更新語句的執行結果處理邏輯足夠簡單,很容易看懂,我就不多說了。

      2.4 小節

      經過前面前面的分析,相信大家對 MyBatis 執行 SQL 的過程都有比較深入的理解。本章的最后,用一張圖 MyBatis 的執行過程進行一個總結。如下:

      在 MyBatis 中,SQL 執行過程的實現代碼是有層次的,每層都有相應的功能。比如,SqlSession 是對外接口的接口,因此它提供了各種語義清晰的方法,供使用者調用。Executor 層做的事情較多,比如一二級緩存功能就是嵌入在該層內的。StatementHandler 層主要是與 JDBC 層面的接口打交道。至于 ParameterHandler 和 ResultSetHandler,一個負責向 SQL 中設置運行時參數,另一個負責處理 SQL 執行結果,它們倆可以看做是 StatementHandler 輔助類。最后看一下右邊橫跨數層的類,Configuration 是一個全局配置類,很多地方都依賴它。MappedStatement 對應 SQL 配置,包含了 SQL 配置的相關信息。BoundSql 中包含了已完成解析的 SQL 語句,以及運行時參數等。

      到此,關于 SQL 的執行過程就分析完了。內容比較多,希望大家耐心閱讀。

      3. 總結

      到這里,本文就接近尾聲了。本篇文章從本月的1號開始寫,一直到16號才寫完初稿。內容之多,完全超出我事先的預計。盡管本文篇幅很大,但仍有部分邏輯和細節沒有分析到,比如 SelectKeyGenerator。對于這些內容,如果大家能耐心看完本文,并且仔細分析了 MyBatis 執行 SQL 的相關源碼,那么對 MyBatis 的原理會有很深的理解。深入理解 MyBatis,對日常工作也會產生積極的影響。比如我現在就以隨心所欲的寫 SQL 映射文件,把不合理的配置統統刪掉。如果遇到 MyBatis 層面的異常,也不用擔心無法解決了。好了,一不小心又扯多了。本篇文章篇幅比較大,這其中可能存在這一些錯誤不妥之處。如果大家發現了,望指明,這里先說聲謝謝。

      好了,本文到此就結束了。感謝大家的閱讀。

      參考

      附錄:MyBatis 源碼分析系列文章列表

      更新時間 標題
      2018-09-11 MyBatis 源碼分析系列文章合集
      2018-07-16 MyBatis 源碼分析系列文章導讀
      2018-07-20 MyBatis 源碼分析 - 配置文件解析過程
      2018-07-30 MyBatis 源碼分析 - 映射文件解析過程
      2018-08-17 MyBatis 源碼分析 - SQL 的執行過程
      2018-08-19 MyBatis 源碼分析 - 內置數據源
      2018-08-25 MyBatis 源碼分析 - 緩存原理
      2018-08-26 MyBatis 源碼分析 - 插件機制

      本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
      作者:田小波
      本文同步發布在我的個人博客:http://www.tianxiaobo.com

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

      posted @ 2018-08-20 08:41  田小波?  閱讀(1539)  評論(2)    收藏  舉報
      主站蜘蛛池模板: 国产95在线 | 欧美| 精品无人区一区二区三区在线| 亚洲精品人妻中文字幕| 福利视频一区二区在线| 强奷乱码欧妇女中文字幕熟女| 国产一二三五区不在卡| 国产女人喷潮视频免费| 久久人人爽爽人人爽人人片av| 免费a级毛片18以上观看精品| 亚洲精品综合久中文字幕| 亚洲日韩中文字幕在线播放| 亚洲精品国产综合久久一线| 中文字幕久久熟女蜜桃| 中文字幕国产精品第一页| 狠狠做五月深爱婷婷天天综合 | 亚洲成人av一区二区| 黄骅市| 黑人av无码一区| 亚洲乱码一二三四区国产| 爆乳日韩尤物无码一区| 在线免费观看视频1区| 国产乱啊有帅gv小太正| 亚洲顶级裸体av片| 中文字幕午夜福利片午夜福利片97| 成人国产精品免费网站| 精品蜜臀国产av一区二区| 丰满少妇被猛烈进出69影院| 中文 在线 日韩 亚洲 欧美| 无码尹人久久相蕉无码| 国内精品久久久久久久coent| 中文字幕亚洲综合第一页| 国产成人精品av| 高清无码爆乳潮喷在线观看| 福利一区二区视频在线| 中文乱码人妻系列一区二区| 亚洲av成人一区二区| 亚洲日本欧美日韩中文字幕| 老妇肥熟凸凹丰满刺激| 久久久精品94久久精品| 偷拍美女厕所尿尿嘘嘘小便| 强奷白丝美女在线观看|