1. 程式人生 > 程式設計 >Mybatis之XML如何對映到方法

Mybatis之XML如何對映到方法

前言

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

XML對映類

前兩篇文章中瞭解到通過Mapper類路徑+方法名對映xxMapper.xml中的namespace+statementID,而namespace+statementID塊其實在初始化的時候在Configuration中儲存在MappedStatement中,所以我們在增刪改查的時候都會看到如下程式碼:

MappedStatement ms = configuration.getMappedStatement(statement);
複製程式碼

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

<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類中相關的屬性:

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中:

  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中判斷的,此處大致說一下其中的原理:遇到**${}和動態標籤如,,則為DynamicSqlSource,否則為StaticSqlSource也就是常見的#{}**;在解析動態sql的時候Mybatis為每個標籤專門提供了處理類NodeHandler,初始化資訊如下:

  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介面中定義的:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}
複製程式碼

這裡的parameterObject就是上文中通過方法引數生成的sql引數物件,這樣BoundSql包含了sql語句,客戶端傳來的引數,以及XML中配置的引數,直接可以進行對映處理;

引數對映

上節中將到了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',javaType=class java.lang.String,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,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,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