1. 程式人生 > 資料庫 >拼多多一面:Mybatis是如何實現SQL語句複用功能的?

拼多多一面:Mybatis是如何實現SQL語句複用功能的?

今天,我們將分析Mybatis之sqlFragment,可以翻譯為sql片段,它的存在價值在於可複用sql片段,避免到處重複編寫。

在工作中,往往有這樣的需求,對於同一個sql條件查詢,首先需要統計記錄條數,用以計算pageCount,然後再對結果進行分頁查詢顯示,看下面一個例子。


<sql id="studentProperties"><!--sql片段-->
    select 
      stud_id as studId
      , name, email
      , dob
      , phone
    from students
  </sql>

  <select id="countAll" resultType="int">
    select count(1) from (
      <include refid="studentProperties"></include><!--複用-->
    ) tmp
  </select>

  <select id="findAll" resultType="Student" parameterType="map">
    select * from (
      <include refid="studentProperties"></include><!--複用-->
    ) tmp limit #{offset}, #{pagesize}
  </select>

這就是sqlFragment,它可以為select|insert|update|delete標籤服務,可以定義很多sqlFragment,然後使用include標籤引入多個sqlFragment。在工作中,也是比較常用的一個功能,它的優點很明顯,複用sql片段,它的缺點也很明顯,不能完整的展現sql邏輯,如果一個標籤,include了四至五個sqlFragment,其可讀性就非常差了。

sqlFragment裡的內容是可以隨意寫的,它不需要是一個完整的sql,它可以是“,phone”這麼簡單的文字。

1. sqlFragment的解析過程

sqlFragment儲存於Configuration內部。

protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

解析sqlFragment的過程非常簡單。

org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)方法部分原始碼。


// 解析sqlFragment
sqlElement(context.evalNodes("/mapper/sql"));
// 為select|insert|update|delete提供服務
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

sqlFragment儲存於Map<String, XNode>結構當中。其實最關鍵的,是它如何為select|insert|update|delete提供服務的。

需要更多大廠面試資料的話也可以暗號:CSDN

2. select|insert|update|delete標籤中,解析include標籤的過程

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()方法原始碼。


// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 重點關注的方法
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

註釋“pre: and were parsed and removed”,含義為解析完,並移除。為什麼要移除呢?祕密都隱藏在applyIncludes()方法內部了。

org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)方法原始碼。


  /**
   * Recursively apply includes through all SQL fragments.
   * @param source Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(Node source, final Properties variablesContext) {
    if (source.getNodeName().equals("include")) {
      // new full context for included SQL - contains inherited context and new variables from current include node
      Properties fullContext;

      String refid = getStringAttribute(source, "refid");
      // replace variables in include refid value
      refid = PropertyParser.parse(refid, variablesContext);
      Node toInclude = findSqlFragment(refid);
      Properties newVariablesContext = getVariablesContext(source, variablesContext);
      if (!newVariablesContext.isEmpty()) {
        // merge contexts
        fullContext = new Properties();
        fullContext.putAll(variablesContext);
        fullContext.putAll(newVariablesContext);
      } else {
        // no new context - use inherited fully
        fullContext = variablesContext;
      }
      // 遞迴呼叫
      applyIncludes(toInclude, fullContext);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 將include節點,替換為sqlFragment節點
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        // 將sqlFragment的子節點(也就是文字節點),插入到sqlFragment的前面
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 移除sqlFragment節點
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      NodeList children = source.getChildNodes();
      for (int i=0; i<children.getLength(); i++) {
        // 遞迴呼叫
        applyIncludes(children.item(i), variablesContext);
      }
    } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
      // replace variables in all attribute values
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
      // replace variables ins all text nodes
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

上面是對原始碼的解讀,為了便於理解,我們接下來採用圖示的辦法,演示其過程。

3. 圖示過程演示

①解析節點


<select id="countAll" resultType="int">
    select count(1) from (
      <include refid="studentProperties"></include>
    ) tmp
  </select>

②include節點替換為sqlFragment節點


<select id="countAll" resultType="int">
    select count(1) from (
        <sql id="studentProperties">
          select 
            stud_id as studId
            , name, email
            , dob
            , phone
          from students
        </sql>
) tmp
  </select>

③將sqlFragment的子節點(文字節點)insert到sqlFragment節點的前面。注意,對於dom來說,文字也是一個節點,叫TextNode。


<select id="countAll" resultType="int">
    select count(1) from (
        select 
            stud_id as studId
            , name, email
            , dob
            , phone
          from students
        <sql id="studentProperties">
          select 
            stud_id as studId
            , name, email
            , dob
            , phone
          from students
        </sql>
) tmp
  </select>

④移除sqlFragment節點


<select id="countAll" resultType="int">
    select count(1) from (
        select 
            stud_id as studId
            , name, email
            , dob
            , phone
          from students
) tmp
  </select>

⑤最終結果如圖所示
在這裡插入圖片描述
(Made In QQ截圖及時編輯)

如此一來,TextNode1 + TextNode2 + TextNode3,就組成了一個完整的sql。遍歷select的三個子節點,分別取出TextNode的value,append到一起,就是最終完整的sql。

這也是為什麼要移除 and 節點的原因。

這就是Mybatis的sqlFragment,以上示例,均為靜態sql,即static sql。

讀者福利

感謝你看到了這裡!
我這邊整理很多2020最新Java面試題(含答案)和Java學習筆記,如下圖
在這裡插入圖片描述

上述的面試題答案小編都整理成文件筆記。 同時也還整理了一些面試資料&最新2020收集的一些大廠的面試真題(都整理成文件,小部分截圖)免費分享給大家,有需要的可以 免費分享~

如果喜歡本篇文章,歡迎轉發、點贊。

記得關注我!
在這裡插入圖片描述