mybatis原始碼學習------resultMap和sql片段的解析

A股慈善家發表於2020-11-02

resultMap的解析

書接上回,mybatis對於<resultMap></resultMap>標籤解析的方法入口為:

在這裡插入圖片描述

resultMapElements(List list)

根據mybatis-3-mapper.dtd檔案中對於resultMap的定義可知,一個mapper節點內可以定義任意多個resultMap節點,所以resultMapElements方法會遍歷所有的resultMap進行解析,程式碼如下:

private void resultMapElements(List<XNode> list) {
  for (XNode resultMapNode : list) {
    try {
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}

resultMapElement(XNode)

呼叫過載方法,三個引數的resultMapElement方法主要用於後續遞迴呼叫,所以這裡單引數的resultMapElement方法更像是一個“橋接”方法。

private ResultMap resultMapElement(XNode resultMapNode) {
  return resultMapElement(resultMapNode, Collections.emptyList(), null);
}

resultMapElement(XNod, List, Class)方法

resultMapElement方法會為<resultMap></resultMap>中配置的每一個或標籤都建立一個ResultMapping物件,並將其儲存在一個集合中,最後傳遞給ResultMapResolver物件進行解析。

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  //因為<resultMap/>標籤的配置存在巢狀的情況,所以在程式碼處理巢狀情況的時候一定會使用遞迴,
  //這裡獲取預設值的邏輯主要是為了處理<resultMap/>巢狀<resultMap/>的情況
  String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
      resultMapNode.getStringAttribute("resultType",
        resultMapNode.getStringAttribute("javaType"))));
  //解析type屬性所配置的類
  Class<?> typeClass = resolveClass(type);
  if (typeClass == null) {
    typeClass = inheritEnclosingType(resultMapNode, enclosingType);
  }
  Discriminator discriminator = null;  //resultMappings集合中維護了當前<resultMap/>中所有的對映關係
  List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {//處理構造器
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {//處理鑑別器
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {//處理其他情況
      List<ResultFlag> flags = new ArrayList<>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  //獲取resultMap節點配置的ID
  //getValueBasedIdentifier()方法返回的格式為    型別1[ID1]_型別2[ID2]......
  String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
  //獲取resultMap節點配置的繼承關係
  String extend = resultMapNode.getStringAttribute("extends");
  //獲取是否開啟自動對映配置
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  //建立ResultMap解析器例項
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    //將具體的解析任務委派給MapperBuilderAssistant的例項
    return resultMapResolver.resolve();
  } catch (IncompleteElementException e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

解析<constructor/>

processConstructorElement方法對建構函式節點中的ID進行了特殊的標識,便於在後續的解析中區分對id和idArg進行區分,如下圖所示,:

在這裡插入圖片描述

private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
  List<XNode> argChildren = resultChild.getChildren();
  for (XNode argChild : argChildren) {
    List<ResultFlag> flags = new ArrayList<>();
    //進行標記
    flags.add(ResultFlag.CONSTRUCTOR);
    if ("idArg".equals(argChild.getName())) {
      flags.add(ResultFlag.ID);
    }
    resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
  }
}

呼叫buildResultMappingFromContext方法進行解析

buildResultMappingFromContext方法

從功能上說,本方法只負責將使用者在xml中配置的資訊讀取出來,解析的部分則交給MapperBuilderAssistant#buildResultMapping去完成

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
  String property;
  if (flags.contains(ResultFlag.CONSTRUCTOR)) {
    //獲取構造方法形參的名字的配置。從 3.4.3 版本開始,通過指定具體的引數名,你可以以任意順序寫入 arg 元素
    property = context.getStringAttribute("name");
  } else {
    //獲取對映到列結果的欄位或屬性的配置
    property = context.getStringAttribute("property");
  }
  //獲取資料庫中的列名,或者是列的別名的配置
  String column = context.getStringAttribute("column");
  //獲取Java類的全限定名,或一個型別別名的配置
  String javaType = context.getStringAttribute("javaType");
  //獲取對應的JDBC型別的配置
  String jdbcType = context.getStringAttribute("jdbcType");
  //獲取巢狀查詢對映語句的ID
  String nestedSelect = context.getStringAttribute("select");
  //獲取巢狀結果集對映的ID,
  String nestedResultMap = context.getStringAttribute("resultMap", () ->
    processNestedResultMappings(context, Collections.emptyList(), resultType));
  //使用者指定非空的列
  String notNullColumn = context.getStringAttribute("notNullColumn");
  //獲取列名字首配置
  String columnPrefix = context.getStringAttribute("columnPrefix");
  //獲取型別攔截器配置
  String typeHandler = context.getStringAttribute("typeHandler");
  //獲取用於載入複雜型別的結果集名字
  String resultSet = context.getStringAttribute("resultSet");
  //獲取指定外來鍵對應的列名
  String foreignColumn = context.getStringAttribute("foreignColumn");
  //巢狀查詢是否懶載入
  boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
  //根據使用者配置解析javaType對應的型別
  Class<?> javaTypeClass = resolveClass(javaType);
  //根據使用者配置解析typeHandler對應的型別
  Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  //根據使用者配置解析jdbcType對應的型別
  JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  //委派給BuilderAssistant例項對其進行進一步的解析
  return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

MapperBuilderAssistant#buildResultMapping

該方法主要對上一步解析出來的配置進行例項化,之後再呼叫ResultMapping.Builder#build()方法對使用者的配置做進一步的解析並建立對應的ResultMapping物件。

public ResultMapping buildResultMapping(
    Class<?> resultType,
    String property,
    String column,
    Class<?> javaType,
    JdbcType jdbcType,
    String nestedSelect,
    String nestedResultMap,
    String notNullColumn,
    String columnPrefix,
    Class<? extends TypeHandler<?>> typeHandler,
    List<ResultFlag> flags,
    String resultSet,
    String foreignColumn,
    boolean lazy) {
  //對結果型別進行型別推斷
  Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
  //對使用者配置的型別攔截器進行例項化
  TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
  //composites集合主要儲存column屬性拆分後生成的結果,在巢狀查詢的模式下,column的配置會作為引數傳遞給目標select語句
  List<ResultMapping> composites;
  if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
    composites = Collections.emptyList();
  } else {
    composites = parseCompositeColumnName(column);
  }
  //使用建造者設計模式建立ResultMapping例項
  return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
      .jdbcType(jdbcType)
      .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
      .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
      .resultSet(resultSet)
      .typeHandler(typeHandlerInstance)
      .flags(flags == null ? new ArrayList<>() : flags)
      .composites(composites)
      .notNullColumns(parseMultipleColumnNames(notNullColumn))
      .columnPrefix(columnPrefix)
      .foreignColumn(foreignColumn)
      .lazy(lazy)
      .build();
}

ResultMapping.Builder#build()方法

該方法的主要邏輯是對使用者的配置進行各種校驗,因為無論當前解析的是

public ResultMapping build() {
  // lock down collections
  resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
  resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
  //如果使用者沒有配置對應的TypeHandler,則mybatis會推斷具體的型別攔截器
  resolveTypeHandler();
  //各種校驗
  validate();
  return resultMapping;
}

validate()方法的定義如下,跟我寫的程式碼一樣拉胯,就不做分析了:

在這裡插入圖片描述

解析 <discriminator>

官網中對於鑑別器的作用的描述如下:

有時候,一個資料庫查詢可能會返回多個不同的結果集(但總體上還是有一定的聯絡的)。 鑑別器(discriminator)元素就是被設計來應對這種情況的,另外也能處理其它情況,例如類的繼承層次結構。 鑑別器的概念很好理解——它很像 Java 語言中的 switch 語句。

解析discriminator的方法為processDiscriminatorElement方法

mybatis在解析discriminator時會建立一個Map來儲存其各個case所對應的resultMap物件,示意圖如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JqzK9U8W-1604318520352)(/Users/lijiaxing/github/mybatis-3-master/doc/build/解析discriminator的示意圖.png)]

  //解析鑑別器配置
  private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    Map<String, String> discriminatorMap = new HashMap<>();
    for (XNode caseChild : context.getChildren()) {
      String value = caseChild.getStringAttribute("value");
      //處理巢狀結果對映
      String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType));
      discriminatorMap.put(value, resultMap);
    }
    //呼叫MapperBuilderAssistant#buildDiscriminator方法來構建Discriminatorshili
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
  }

processNestedResultMappings方法

該方法會建立鑑別器map集合中value所指定的resultMap物件

//處理巢狀結果對映
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
  if (Arrays.asList("association", "collection", "case").contains(context.getName())
    && context.getStringAttribute("select") == null) {
    //校驗相關規則
    validateCollection(context, enclosingType);
    //校驗通過後解析對應的resultMap節點
    ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
    return resultMap.getId();
  }
  return null;
}

MapperBuilderAssistant#buildDiscriminator

public Discriminator buildDiscriminator(
    Class<?> resultType,
    String column,
    Class<?> javaType,
    JdbcType jdbcType,
    Class<? extends TypeHandler<?>> typeHandler,
    Map<String, String> discriminatorMap) {
  //構建ResultMapping物件
  ResultMapping resultMapping = buildResultMapping(
      resultType,
      null,
      column,
      javaType,
      jdbcType,
      null,
      null,
      null,
      null,
      typeHandler,
      new ArrayList<>(),
      null,
      null,
      false);
  Map<String, String> namespaceDiscriminatorMap = new HashMap<>();
  //遍歷map的value,為每一個resultMap的id加上namespace
  for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
    String resultMap = e.getValue();
    resultMap = applyCurrentNamespace(resultMap, true);
    namespaceDiscriminatorMap.put(e.getKey(), resultMap);
  }
  //通過建造者模式建立Discriminator物件
  return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
}

解析其他標籤

處理其他情況,如<id><result><association>等標籤的程式碼如下,呼叫buildResultMappingFromContext方法建立對應的ResultMapping物件,並將其儲存在對應的list中即可。

在這裡插入圖片描述

建立並儲存ResultMap物件

回顧resultMapElement方法的邏輯,將配置解析為ResultMapping物件並新增到集合中後,接下來要做的就是建立對應的ResultMap物件,並儲存在configuration中。

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
  //省略
  
  //獲取resultMap節點配置的ID
  //getValueBasedIdentifier()方法返回的格式為    型別1[ID1]_型別2[ID2]......
  //用於處理resultMap巢狀時,內層resultMap沒有id的情況
  String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
  //獲取resultMap節點配置的繼承關係
  String extend = resultMapNode.getStringAttribute("extends");
  //獲取是否開啟自動對映配置
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  //建立ResultMap解析器例項
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    //將具體的解析任務委派給MapperBuilderAssistant的例項
    return resultMapResolver.resolve();
  } catch (IncompleteElementException e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

由上面的邏輯可知,XmlMapperBuilder在這裡建立了一個ResultMapResolver物件,並通過該物件來完成ResultMap物件建立和儲存工作。

ResultMapResolver類的定義

public class ResultMapResolver {
  private final MapperBuilderAssistant assistant;
  private final String id;
  private final Class<?> type;
  private final String extend;
  private final Discriminator discriminator;
  private final List<ResultMapping> resultMappings;
  private final Boolean autoMapping;

  public ResultMapResolver(MapperBuilderAssistant assistant, String id, Class<?> type, String extend, Discriminator discriminator, List<ResultMapping> resultMappings, Boolean autoMapping) {
    this.assistant = assistant;
    this.id = id;
    this.type = type;
    this.extend = extend;
    this.discriminator = discriminator;
    this.resultMappings = resultMappings;
    this.autoMapping = autoMapping;
  }

  public ResultMap resolve() {
    //委派給MapperBuilderAssistant例項完成建立和儲存的工作
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
  }
}

MapperBuilderAssistant#addResultMap

public ResultMap addResultMap(
    String id,
    Class<?> type,
    String extend,
    Discriminator discriminator,
    List<ResultMapping> resultMappings,
    Boolean autoMapping) {
  //將ID替換為帶有名稱空間的格式
  id = applyCurrentNamespace(id, false);
  //將當前resultMap所繼承的resultMap的ID替換為帶有名稱空間的格式
  extend = applyCurrentNamespace(extend, true);
  //處理繼承樹
  if (extend != null) {
    if (!configuration.hasResultMap(extend)) {
      throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
    }
    ResultMap resultMap = configuration.getResultMap(extend);
    //獲取父節點中配置的所有ResultMapping物件
    List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
    //移除重複的ResultMapping配置
    extendedResultMappings.removeAll(resultMappings);
    //如果子resultMap節點中宣告瞭<constructor><constructor/>標籤,則需要將父節點中的構造器標籤配置刪除
    boolean declaresConstructor = false;
    for (ResultMapping resultMapping : resultMappings) {
      if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
        declaresConstructor = true;
        break;
      }
    }
    if (declaresConstructor) {
      //移除父節點中的構造器配置
      extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
    }
    //合併父子<resultMap>節點的ResultMapping配置
    resultMappings.addAll(extendedResultMappings);
  }
  //通過建造者設計模式建立ResultMap物件,並將其新增到mybatis的全域性配置物件中
  ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
      .discriminator(discriminator)
      .build();
  configuration.addResultMap(resultMap);
  return resultMap;
}

Configuration#addResultMap

後面兩個方法暫時沒看懂是什麼意思,先挖個坑,後面再來填

public void addResultMap(ResultMap rm) {
  resultMaps.put(rm.getId(), rm);
  //TODO
  checkLocallyForDiscriminatedNestedResultMaps(rm);
  //TODO
  checkGloballyForDiscriminatedNestedResultMaps(rm);
}

sql片段的解析

解析sql片段的程式碼入口為XmlMapperBuilder#sqlElement
在這裡插入圖片描述

sqlElement

private void sqlElement(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}
//如果使用者配置了databaseId,則會檢測當前databaseId與configuration中配置的databaseId是否一致,
//一致則新增到sqlFragments屬性中
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    //給id拼接namespace
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      //如果匹配則將其新增到sqlFragments欄位中
      sqlFragments.put(id, context);
    }
  }
}

databaseIdMatchesCurrent

//資料庫廠商標識是否與當前sql片段的定義相同
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
  //如果需要的資料庫廠商ID不為空
  if (requiredDatabaseId != null) {
    return requiredDatabaseId.equals(databaseId);
  }
  //隱含條件為需要的資料庫廠商ID為空
  if (databaseId != null) {
    return false;
  }
  //如果sqlFragments集合中不存在對應的sql片段定義
  if (!this.sqlFragments.containsKey(id)) {
    return true;
  }
  //如果存在ID對應的sql片段
  XNode context = this.sqlFragments.get(id);
  //根據上面的if語句可以得出以下,如果程式能走到這裡,則requiredDatabaseId一定為null,故直接判斷databaseId屬性的值是否為null即可
  return context.getStringAttribute("databaseId") == null;
}

相關文章