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如下圖所示:
不管是動態還是靜態的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,快取等;後面每個細節都會單獨的寫一篇文章來介紹。