Mybatis之XML如何映射到方法

前言

上文Mybatis之方法如何映射到XML中介紹了Mybatis是如何將方法進行分拆出方法名映射到statementID,參數如何解析成xml中sql所須要的,以及返回類型的處理;本文將從XML端來看是如何同方法端進行映射的。html

XML映射類

前兩篇文章中瞭解到經過Mapper類路徑+方法名映射xxMapper.xml中的namespace+statementID,而namespace+statementID塊其實在初始化的時候在Configuration中保存在MappedStatement中,因此咱們在增刪改查的時候都會看到以下代碼:java

MappedStatement ms = configuration.getMappedStatement(statement);

在Configuration中獲取指定namespace+statementID的MappedStatement,而在Configuration是經過Map維護了對應關係;已最多見的Select語句塊爲例,在XML中的配置的結構以下:node

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
  ...sql語句...
</select>

其餘增刪改除了個別的幾個關鍵字好比:keyProperty,keyColumn等,其餘和select標籤相似;再來看一下MappedStatement類中相關的屬性:git

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
  ...省略...
}

select標籤裏面的關鍵字基本能夠在類MappedStatement中找到對應的屬性,關於每一個屬性表明的含義能夠參考官方文檔:mybatis-3;除了關鍵字還有sql語句,對應的是MappedStatement中的SqlSource,sql語句有動態和靜態的區別,對應的SqlSource也提供了相關的子類:StaticSqlSource和DynamicSqlSource,相關的sql解析類在XMLScriptBuilder中:github

public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

具體哪一種sql是動態的,哪一種是靜態的,相關邏輯在parseDynamicTags中判斷的,此處大體說一下其中的原理:遇到${}和動態標籤如<if>,<set>,<foreach>則爲DynamicSqlSource,不然爲StaticSqlSource也就是常見的#{};在解析動態sql的時候Mybatis爲每一個標籤專門提供了處理類NodeHandler,初始化信息以下:sql

private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

處理完以後會生成對應的SqlNode以下圖所示:
xx.jpg數據庫

無論是動態仍是靜態的SqlSource,最終都是爲了獲取BoundSql,如SqlSource接口中定義的:express

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

這裏的parameterObject就是上文中經過方法參數生成的sql參數對象,這樣BoundSql包含了sql語句,客戶端傳來的參數,以及XML中配置的參數,直接能夠進行映射處理;segmentfault

參數映射

上節中將到了BoundSql,本節重點來介紹一下,首先能夠看一下其相關屬性:緩存

public class 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語句,xml配置的參數映射,客戶端傳來的參數以及額外參數;已一個常見的查詢爲例能夠看一下大體的內容:

<select id="selectBlog3" parameterType="hashmap" resultType="blog">
        select * from blog where id = #{id} and author=#{author,jdbcType=VARCHAR,javaType=string}
    </select>

此時sql對應就是:

select * from blog where id = ? and author=?

parameterMappings對應的是:

ParameterMapping{property='id', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='author', mode=IN, javaType=class java.lang.String, jdbcType=VARCHAR, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

parameterObject對應的是:

{author=zhaohui, id=158, param1=158, param2=zhaohui}

若是知道以上參數,咱們就能夠直接使用原生的PreparedStatement來操做數據庫了:

PreparedStatement prestmt = conn.prepareStatement("select * from blog where id = ? and author=?");
prestmt.setLong(1,id);
prestmt.setString(2,author);

其實Mybatis本質上和上面的語句沒有區別,能夠看一下Mybatis是如何處理參數的,具體實如今DefaultParameterHandler中,以下所示:

public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            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 {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

大體就是遍歷parameterMappings,而後經過propertyName到客戶端參數parameterObject中獲取對應的值,獲取到值以後就面臨一個問題一個就是客戶端參數的類型,另外一個就是xml配置的類型,如何進行轉換,Mybatis提供了TypeHandler來進行轉換,這是一個接口類,其實現包括了經常使用的基本類型,Map,對象,時間等;具體使用哪一種類型的TypeHandler,根據咱們在xml中配置的<javaType=類型>來決定,若是沒有配置則使用UnknownTypeHandler,UnknownTypeHandler內部會根據value的類型來決定使用具體的TypeHandler;Mybatis內部全部的類型都註冊在TypeHandlerRegistry中,因此獲取的時候直接根據value類型直接去TypeHandlerRegistry獲取便可;獲取以後直接調用typeHandler.setParameter(ps, i + 1, value, jdbcType),已StringTypeHandler爲例,能夠看一下具體實現:

public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

使用了原生的PreparedStatement,往指定位置設置參數值;設置完參數以後就執行execute方法,返回結果;

結果映射

上一節執行execute以後,返回的是ResultSet結果集,若是直接用原生的讀取方式,你會看到以下代碼:

ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
      Long id = resultSet.getLong("id");
      String title = resultSet.getString("title");
      String author = resultSet.getString("author");
      String content = resultSet.getString("content");
      ......
}

獲取到每一個字段的數據以後,而後經過反射的方式生成一個對象;Mybatis內部其實也是這樣實現的,封裝好經過簡單的配置便可獲取結果集,常見的結果集配置如resultMap,resultType等;Mybatis內部處理ResultSet是ResultSetHandler,其具體實現是DefaultResultSetHandler類:

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

處理接口中一共定一個了三個方法分別是:處理普通的結果集,處理遊標結果集,以及處理輸出參數,能夠大體看一下最經常使用的handleResultSets實現:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    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++;
    }
    ...如下省略...
  }

while循環遍歷結果集,ResultMap就是在XML中定義的結果集,好比定一個的是一個類型Blog,那麼會在處理結果的時候,首先建立一個對象,而後給對象屬性分配類型處理器TypeHandler,而後根據實際類型調用處理器的getResult方法:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

能夠看到類型處理器分別用來處理設置參數和從結果集獲取參數,也就是分別處理了輸入和輸出;處理完以後其實就生成了XML中配置的結果集,多是一個對象,列表,hashMap等;另一個須要注意的地方就是xxMapper接口中定義的返回值須要保證和XML中配的結果集一致,否則當咱們經過代理對象返回結果集的時候會出現類型轉換異常;

總結

XML的映射本文分三塊來介紹的,分別從Statement塊映射MappedStatement,參數映射ParameterMapping,以及結果集是如何經過DefaultResultSetHandler處理的;固然本文只是介紹了一個大概的映射流程,不少細節沒有講到,好比ResultHandler,RowBounds,緩存等;後面每一個細節都會單獨的寫一篇文章來介紹。

示例代碼地址

Github

相關文章
相關標籤/搜索