1. 程式人生 > 其它 >Mybatis原始碼解析:sql引數處理,原來可以這麼簡單

Mybatis原始碼解析:sql引數處理,原來可以這麼簡單

技術標籤:JavamybatisMySQLjavamybatissql

在這個章節中我們討論當sql帶有引數時,Mybatis是如何處理的。使用的還是User類。

Mybatis原始碼解析:sql引數處理,原來可以這麼簡單

//省略get set方法
public class User {
    private int id;
    private String name;
    private String phone;
}

例1 帶有全域性變數的sql

//UserMapper中的dao介面
List<User> getByglobal();
  <select id="getByglobal" resultType="com.entity.User">
      select * from user where id = ${globalId}
  </select>
<!--mybatis.xml中的部分配置-->
  <properties>
    <property name="globalId" value="1"/>
  </properties>

注意我是用的符號為$。在這個例子中globalId是在mybatis.xml檔案中的property配置的。介面不傳引數。

在學習第二個章節時,我們知道每一個查詢語句都會被包裝成一個MappedStatement,這個物件用來存放我們的sql語句,返回型別,id等等。讓我們回到之前的程式碼。

  //XMLMapperBuilder.configurationElement
  private void configurationElement(XNode context) {
    try {
      //mapper的快取資訊,名稱空間等會被臨時儲存到MapperBuilderAssistant中,最後把這些公用的資訊在存到MappedStatement中
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      //該節點已經被廢棄
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //在解析增刪改查節點時,每個節點都會生成一個mapperStatement物件並儲存到配置檔案類中.
      //mapperStatement儲存這這個節點的全部資訊,如id,fetchSize,timeout
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

這部分程式碼應該不陌生,第二章分析了buildStatementFromContext()方法,現在我們從context.evalNodes("select|insert|update|delete")開始。該方法開始解析增刪改查的節點

  //XNode.evalNodes
  public List<XNode> evalNodes(String expression) {
    return xpathParser.evalNodes(node, expression);
  }
  public List<XNode> evalNodes(Object root, String expression) {
    List<XNode> xnodes = new ArrayList<>();
    NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      //這裡建立的新的節點並新增到結合中,解析也是在建立節點的時候開始的。
      xnodes.add(new XNode(this, nodes.item(i), variables));
    }
    return xnodes;
  }

XNode構造方法,在建立新節點的時候,會在構造器中先進性解析,也就是呼叫parseBody方法.

  public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    this.name = node.getNodeName();
    this.variables = variables;
    //獲取節點的屬性
    //例如這個select就有id和resultType兩個屬性
    this.attributes = parseAttributes(node);
    //解析節點裡面的內容,也就是sql了
    this.body = parseBody(node);
  }

variables傳入配置中的全域性變數

  //XNode.parseBody
  private String parseBody(Node node) {
    //獲取當前節點的資訊
    //例子中這裡返回空
    String data = getBodyData(node);
    if (data == null) {
      //獲取孩子節點的資訊
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        //獲取當前節點的資訊
        data = getBodyData(child);
        if (data != null) {
          break;
        }
      }
    }
    return data;
  }

為什麼select節點getBodyData會返回空呢,從它的方法體中可以看出,首先它會判斷節點的型別,select這個節點是ELEMENT_NODE型別,不屬於它要求的文字型別或者部分節點型別。那麼就直接返回空了。而當select的孩子節點,也就是sql語句select * from user where id = ${globalId}這個節點呼叫getBodyData方法時,sql語句是文字型別的,滿足條件,才會使用解析器開始解析。

  //XNode.getBodyData
  private String getBodyData(Node child) {
    //判斷節點的型別
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
      String data = ((CharacterData) child).getData();
      data = PropertyParser.parse(data, variables);
      return data;
    }
    return null;
  }
  //PropertyParser.parse
  public static String parse(String string, Properties variables) {
    //先建立一個處理器
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //建立解析器
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    //進行解析
    return parser.parse(string);
  }

這裡出現了很多陌生的類。首先是GenericTokenParser通用型別的解析器,他能根據傳入的引數做出相應。如果引數滿足條件,就會呼叫handler處理器來處理引數。每個handler都要實現handleToken方法,該方法就是用來處理引數的。

例如這裡傳入的是以${作為開頭,}作為結尾。如果傳入的字串包含一個或者多個這樣的格式,就會呼叫VariableTokenHandler.handleToken,該方法會試圖從全域性中找到該變數,並修改成具體的值。

VariableTokenHandler.handleToken 傳入String變數globalId,將其替換成1並返回。

    public String handleToken(String content) {
     //variables裡面存放全域性的變數,為空直接return
      if (variables != null) {
        String key = content;
        //是否存在預設值,預設是false
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        //variables是用來存放全域性變數的容器。
        //這裡會從全域性變數中找到我們定義的globalId,然後將對應的值返回,這樣我們的sql就拼接完成了
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }
  

解析器程式碼,根據傳入的標記開始解析,這裡傳入開始標記${和結束標記$}。在這之後還會用來解析#{}。程式碼比較長,最好打個斷點進去看。

//GenericTokenParser.parse
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    //查詢開始標記,如果不存在返回-1 ,存在返回偏移量
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    //這個變數用來存放中間的字元,如${id}中的id
    StringBuilder expression = null;
    //如果存在開始標誌
    while (start > -1) {
      //這裡將從offset開始,一直到start的字元先放入builder中
      //例如select * from user where id = 
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        //更新偏移量
        offset = start + openToken.length();
        //找到與開始標誌對應的結束標誌
        int end = text.indexOf(closeToken, offset);
        //取到中間字元globalId
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //這裡根據不同的處理器會有不同的操作,剛才傳入的是VariableTokenHandler
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

到這裡全域性變數就解析完成了,那麼如果在全域性變數中沒有找到對應的值該怎麼辦呢?例如我這裡使用的sql是select * from user where id = ${id},而不是${globalId},那麼根據VariableTokenHandler處理器,它會原封不動的進行返回,等待後文的解析。

順便一提,這一部分的解析實在解析我們的配置檔案的時候就發生了,方法入口為context.evalNodes("select|insert|update|delete"),在解析配置的時候,其他節點也大量使用了context.evalNodes()方法去解,所以只要當配置mybatis.xml檔案中的properties節點解析完成之後,裡面的變數就是能全域性使用了,這也是為什麼properties節點要放在第一個解析。

又由於這個通用解析器只解析${XXX}格式的變數,所以全域性的變數不能寫成#{xxx}.

入參${}的解析

List<User> get(Integer id);
  <select id="get" resultType="com.entity.User">
      select * from user where id = ${id}
  </select>

這個例子,我們沒有在全域性變數中定義id,而是在方法中傳入這個值。根據上文中的VariableTokenHandler.handleToken方法就會返回${id},表示這個引數全域性變數中沒有,是待解析的引數。

這是解析buildStatementFromContext(context.evalNodes("select|insert|update|delete"));的後續程式碼,用來解析標籤,並建立mappedStaement,在第二章中也分析過,這裡直接copy過來.

  //XMLStatementBuilder.parseStatementNode
  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否重新整理快取 預設值:增刪改重新整理 查詢不重新整理
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否使用二級快取 預設值:查詢使用 增刪改不使用
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //是否需要處理巢狀查詢結果 group by

    // 三組資料 分成一個巢狀的查詢結果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替換Includes標籤為對應的sql標籤裡面的值
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定義指令碼語言驅動 mybatis plus
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

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

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //設定主鍵自增規則
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    //解析Sql  根據sql文字來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //暗示驅動程式每次批量返回的結果行數
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超時時間
    Integer timeout = context.getIntAttribute("timeout");
    //引用外部 parameterMap,已廢棄
    String parameterMap = context.getStringAttribute("parameterMap");
    //結果型別
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    //引用外部的 resultMap
    String resultMap = context.getStringAttribute("resultMap");
    //結果集型別,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一種
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    //(僅對 insert 有用) 標記一個屬性, MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設定它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  

找到解析sql的部分具體來分析,一層一層往下。

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

RawLanguageDriver.createSqlSource 該類是XMLLanguageDriver的子類

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    checkIsNotDynamic(source);
    return source;
  }

XMLLanguageDriver.createSqlSource

  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

XMLScriptBuilder.parseScriptNode

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    //判斷節點是否是動態的,包含是否包含if、where 、choose、trim、foreach、bind、sql標籤,這個例子中我們進入else
    if (isDynamic) {
      //不解析
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用佔位符方式來解析
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

這裡進行判斷isDynamic的值,這個方法我們只需要關注textSqlNode.isDynamic()就行了。程式碼與之前解析node有些類似。

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      //注意!!這裡又new了一個XNode,也就是說,這個節點中的sql語句又被解析了一次,解析方式和上文從同全域性獲取變數一樣。
      //與上文不同的是,這裡傳入的是子節點,也就是sql文字語句,而上文解析的是整個select元素
      //這個child是臨時變數,節點解析的結果不做儲存
      XNode child = node.newXNode(children.item(i));
      //判斷節點型別
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //這裡判斷語句是否是動態的
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

TextSqlNode.isDynamic

  public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    //這裡建立一個解析器進行解析sql語句,這裡解析的是仍然是${}
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }
  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

熟悉的程式碼,還是同樣的解析器,用來處理${,和},不過這次的hander不同,為DynamicCheckerTokenParser

  //DynamicCheckerTokenParser.handleToken
  public String handleToken(String content) {
    this.isDynamic = true;
    return null;
  }
}

這次的處理方式是將直接返回空,也就是說,sql會變成 select * from user where id = null。但是返回的結果並沒有被儲存,parser.parse(text)並沒有引數來接受它的返回值,所以這裡只是用來更新isDynamic引數。

回到XMLScriptBuilder.parseScriptNode方法,這裡根據isDynamic的布林值,會有兩種SqlSource.DynamicSqlSource和RawSqlSource。到這裡配置檔案就解析完成了,後續sql中的引數都是從方法中獲取的,所以只能在執行的時候動態進行替換。

來到query查詢方法,方法在第三章執行sql的時候簡單說過。ms.getBoundSql會獲取繫結的封裝sql.

//CachingExecutor.query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

MappedStatement.getBoundSql

  public BoundSql getBoundSql(Object parameterObject) {
    //獲取繫結的sql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //獲取sql中對應的引數
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }
//DynamicSqlSource.getBoundSql。
  public BoundSql getBoundSql(Object parameterObject) {
    //parameterObject中有我們方法傳入的引數
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //這裡解析${}
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

為什麼是DynamicSqlSource而不是RawSqlSource,這個前文分析過,在替換完全域性變數後,語句中如果還包含${},使用的就是DynamicSqlSource。

  //MixedSqlNode.apply
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }

TextSqlNode.apply

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }

這裡再次建立了${}的解析器,這次的handler是BindingTokenParser

  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

BindingTokenParser.handleToken,如果sql中存在${},就會將其替換成具體的引數,語句就變成 select * from user where id = 1,就能直接執行了

  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);
    }
    Object value = OgnlCache.getValue(content, context.getBindings());
    String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
    checkInjection(srtValue);
    return srtValue;
  }

入參#{}的解析

那麼如果是#{}該怎麼處理呢?

  <select id="get" resultType="com.entity.User">
      select * from user where id = #{id}
  </select>
  List<User> get(Integer id);

由上文得知,由於沒有${},那麼SqlSource就會變成RawSqlSource。在建立RawSqlSource的時候,在構造方法中就會對#{}解析。

RawSqlSource的構造方法。

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

SqlSourceBuilder.parse

  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);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

這裡用的hander是ParameterMappingTokenHandler,它的作用是將#{XXX}替換成

ParameterMappingTokenHandler.handleToken

    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

這時sql就變成了select * from user where id = ?,到這裡還只是解析配置檔案。在具體執行方法時也要呼叫getBoundSql方法將引數進行賦值

  //RawSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

StaticSqlSource.getBoundSql,最後呼叫BoundSql的構造方法,將sql語句,入參等傳入

  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

之後就要建立資料庫連線,進行查詢了。回到這個方法SimpleExecutor.prepareStatement。回顧一下,這是建立StatementHandler後做的一些連線資料庫的準備操作。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  //獲取jdbc資料庫連線
  Connection connection = getConnection(statementLog);
  //一些準備工作,初始化Statement連線
  stmt = handler.prepare(connection, transaction.getTimeout());
  //使用ParameterHandler處理入參
  handler.parameterize(stmt);
  return stmt;
}

我們先進入這個方法PreparedStatementHandler.parameterize。

為什麼是PreparedStatementHandler之前也說過,因為語句的預設型別是PREPARED, 還有其他的型別如果是CALLABLE,對應CallableStatementHandler,STATEMENT對應SimpleStatementHandler。可以用引數statementType進行設定。

@Override
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}

DefaultParameterHandler.setParameters.

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //boundSql用來解析我們的sql語句,parameterMappings是我們傳入的引數
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        //這裡第一個引數就是id
        ParameterMapping parameterMapping = parameterMappings.get(i);
        //mode屬性允許能指定IN,OUT或INOUT引數。如果引數的 mode 為 OUT 或 INOUT,將會修改引數物件的屬性值,以便作為輸出引數返回。
        //#{id}預設mode為OUT
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          //這裡是boundsql中的額外引數,可以使用攔截器新增,例子放在下文
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
            //如果型別處理器中有這個型別,那麼直接賦值就行了,例如這裡是Integer型別,型別處理器是有的
            //那麼直接賦值
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
          //如果不是的會轉化為元資料進行處理,metaObject元資料可以理解為用來反射的工具類,可以處理引數的get,set
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          //獲取型別處理器
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          //獲取資料庫型別
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            //使用不同的型別處理器向jdbc中的PreparedStatement設定引數
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

value如果是空的那麼就直接設定為jdbc的空型別,不為空呼叫具體的型別處理器。

BaseTypeHandler.setParameter。該類是所有typeHandler的父類.如果不為空呼叫setNonNullParameter,該方法時抽象的,由具體的子類實現。這裡使用的是一個相當於路由的的子類UnknownTypeHandler,這個子類可以根據傳入的型別,再去找到具體的型別處理器,例如IntegerTypeHander.

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
  }

UnknownTypeHandler.setNonNullParameter

  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

UnknownTypeHandler.resolveTypeHandler這個方法根據傳入的引數型別,找到具體的TypeHandler

  private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {
      handler = OBJECT_TYPE_HANDLER;
    } else {
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {
        handler = OBJECT_TYPE_HANDLER;
      }
    }
    return handler;
  }

例如如果這個引數是id,Integer型別,那麼就會找到IntegerTypeHandler

  //IntegerTypeHandler
  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter);
  }

最後還是使用jdbc的PreparedStatement處理引數。

附:自定義的攔截器用來加入引數。

@Intercepts({
  @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler bs = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = bs.getBoundSql();
    boundSql.setAdditionalParameter("id","1");
    return invocation.proceed();
  }
}

例4 ${}和#{}都存在的情況

如果是都存在的情況呢?

  <select id="findUserByIdAndName" resultType="com.entity.User">
        select * from user where id = ${id} AND name = #{name}
    </select>
  List<User> findUserByIdAndName(@Param("id") Integer id, @Param("name") String name);

結合上文的分析,由於存在${},所以選擇的DynamicSqlSource。

DynamicSqlSource.getBoundSql。這個方法上文分析到了rootSqlNode.apply(context);會將${}替換成具體引數。我們接著分析。

  public BoundSql getBoundSql(Object parameterObject) {
    //parameterObject中有我們方法傳入的引數
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //解析${}並替換成具體的值
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //這裡又進行了一次解析
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

解析#{},並將其替換成?

  //RawSqlSource.parse
  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);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

這樣我們的語句就變成了select * from user where id = 1 AND name = ?,然後呼叫sqlSource.getBoundSql

  //StaticSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

最後的處理方式與例3相同,使用jdbc自帶的PreparedStatement進行引數處理。

小結

當我們在解析mapper.xml檔案時,就會將sql進行第一遍的解析,將其中的全域性變數替換成具體的值。

接著進行第二遍的解析,選擇不同的SqlSource。這一邊的解析不改變語句中的sql內容。

如果語句中包含${},就選擇DynamicSqlSource,等待具體執行sql的時候再做處理.如果僅包含#{}型別的,就選擇RawSqlSource。RawSqlSource在建立的時候就會有進行一輪的解析,將語句中的#{XXX}替換為 ?(問號)

之後在執行具體的語句才動態的替換,如果之前選擇的是DynamicSqlSource,那麼進行兩次的解析,第一次將${}替換成具體值,第二次解析#{},使用jdbc的PreparedStatement處理。如果選擇的是RawSqlSource,那麼這條語句就只有#{},直接用PreparedStatement處理。

可以發現,無論什麼型別的sql都會被解析了4次。