官網上面講到:無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。那么為什么會有類型處理器呢?這一點並不難理解,SQL語句事實上可以理解為一門面向數據庫的編程語言。所以相對而言都有自己的數據類型。這也就意味着存在數據類型不一至的問題。同時不同的數據庫之間數據類型還有一定的差義。類型處理器則就是用於處理數據類型不一至的問題。
筆者看過幾個不同的ORM框架都存在着類型處理器的概念。可見類型處理器在ORM框架上實現有多么重要。在官網上面已經列出了20多種的內部類型處理器。筆者建議最好選擇性的看。好比如說筆者當前的列子里面用到的一個叫UnknownTypeHandler的類型處理器。
List<Product> products = dao.SelectProducts(30, "a");
<select id="SelectProducts" resultMap="result" > select * from Products where #{0} > ProductID and ProductName like #{1} </select>
例子里面並沒有指出是什么樣子的JAVA類型。所以當然是UnknownTypeHandler類型了。即然這樣子我們不煩設置一下他的類型在來看看。我們只要把上面的配置修改一下就可以了。如下紅色標記。
<select id="SelectProducts" resultMap="result" > select * from Products where #{0,javaType=int,jdbcType=NUMERIC} > ProductID and ProductName like #{1,javaType=String,jdbcType=VARCHAR} </select>
關於#{}的語法問題,相信筆者不用多講大家都清楚。從上面簡單的設置里,我們可以看到源碼里面會找到IntegerTypeHandler和StringTypeHandler類型處理器來設置參數。類型處理器都是實現於TypeHandler接口,源碼都存放在org.apache.ibatis.type的命名空間下。TypeHandler接口的源碼也很簡單。我們可以看到setParameter方法。顧名思義他就是用於設置參數。其他的方法都是用於處理回返的結果。如下。
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; }
想要了解源碼,就要找一個合適的入切類——筆者選擇IntegerTypeHandler類。IntegerTypeHandler類並沒有直接的實現了TypeHandler接口。而是繼承了BaseTypeHandler類。正如上面所講的,BaseTypeHandler類的setParameter方法我們必須了解一下。如下
1 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { 2 if (parameter == null) { 3 if (jdbcType == null) { 4 throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters."); 5 } 6 try { 7 ps.setNull(i, jdbcType.TYPE_CODE); 8 } catch (SQLException e) { 9 throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " + 10 "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " + 11 "Cause: " + e, e); 12 } 13 } else { 14 try { 15 setNonNullParameter(ps, i, parameter, jdbcType); 16 } catch (Exception e) { 17 throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " + 18 "Try setting a different JdbcType for this parameter or a different configuration property. " + 19 "Cause: " + e, e); 20 } 21 } 22 }
源碼的意思很簡單——把參數分為非空值和空值來分別設置。空值的設置方法是JDBC內部自己的。而非空值的方法是mybatis自己封裝的。所以我們可以看到setNonNullParameter方法是一個抽象方法。看樣子我們可以猜測的出來——真正的實現是放在子類的setNonNullParameter方法里面。
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
來看一下IntegerTypeHandler類的setNonNullParameter方法。
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter); }
事實上筆者認為IntegerTypeHandler類沒有什么可看的。但是UnknownTypeHandler類的處理卻得我們去研究。我們可以先想一下——不知道類型的情況下,如何知道需要的類型處理器。
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { TypeHandler handler = resolveTypeHandler(parameter, jdbcType); handler.setParameter(ps, i, parameter, jdbcType); }
不難看出他又根據參數值和JDBC數據類型來判斷獲得相應的類型處理器。然后在進行參數設值。所以我們的目標移向resolveTypeHandler方法。
1 private TypeHandler<? extends Object> resolveTypeHandler(Object parameter, JdbcType jdbcType) { 2 TypeHandler<? extends Object> handler; 3 if (parameter == null) { 4 handler = OBJECT_TYPE_HANDLER; 5 } else { 6 handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType); 7 // check if handler is null (issue #270) 8 if (handler == null || handler instanceof UnknownTypeHandler) { 9 handler = OBJECT_TYPE_HANDLER; 10 } 11 } 12 return handler; 13 }
源碼的意思是判斷傳入的參數值是不是為空的,空則調用ObjectTypeHandler類型處器。如果不為空,根據參數傳入的類型和當前JDBC數據類型來獲得相應的類型處理器。如果還是找不到或是還是UnknownTypeHandler類,則調用ObjectTypeHandler類型處器。UnknownTypeHandler類實際沒有設置參數值的功能。而是由其他的類型處理器來實現。
好了,到了這里面對於類型處理器,相信大家都有了一定的理解。但是這里還有一個問題值得我們去研究一下——類型處理器是什么時候創建的。想要了解他的來源就必須回頭去看一下DefaultParameterHandler類的setParameters方法。也就是上一章的結尾處相同的代碼。
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); } } } }
BoundSql類上一章筆者也有講到。他是用於存放sql語句信息。為什么這里要提到他呢?相信從源碼里面我們可以看到類型處理器是通過ParameterMapping類來獲得的。而ParameterMapping類又是通過 BoundSql類獲得。所以就不得不去了解一下BoundSql類是如何獲得ParameterMapping類的。
關於如何BoundSql類是如何獲得ParameterMapping類的。事實這一塊跟XMLMapperBuilder類有關系。XMLMapperBuilder類就是用於加載Mapper文件的。而他的configurationElement方法。正是我們現在需要了解的。如下
private void configurationElement(XNode context) { try { 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")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
最后面的buildStatementFromContext方法是了解如何獲得ParameterMapping類的開頭。這句代碼中已經很明顯的看出他是用於加載select,insert等節點信息。當我們繼續跟進去的話,最終會看到一個了parseStatementNode方法。這一個過程讀者們自行查看。
1 processSelectKeyNodes(id, parameterTypeClass, langDriver); 2 3 // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) 4 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); 5 String resultSets = context.getStringAttribute("resultSets"); 6 String keyProperty = context.getStringAttribute("keyProperty"); 7 String keyColumn = context.getStringAttribute("keyColumn");
筆者只是貼出一部分的代碼。紅色標記的內容是我們所要關注的重點。(其外parseStatementNode方法還是了解如何創建MappedStatement類的入口類。)筆者為什么要提到SqlSource接口呢?我們都知道在mybatis框架里面的sql語句決不可能只有sql語句。還會出現if之類的語法。那么這種sql語句,mybatis框架叫他為動態 SQL。而BoundSql類就是通過SqlSource接口所繼承的類獲得。
mybatis對sql語句定義為三種半。為什么是三種半呢?因為還有一個可能正在研發中。每一種在源碼中都有相關的類。
1.DynamicSqlSource類用於動態SQL。
2.StaticSqlSource類用於靜態SQL。
3.ProviderSqlSource類用於腳本語言。
4.VelocitySqlSource類用於XXX。還是讀者們自己去看吧。
SqlSource接口的源碼才是筆者為什么要講SqlSource接口的最好證明。如下
public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }
我們搞清楚了BoundSql類是哪里來了之后。我們在去了解如何獲得ParameterMapping類就變得簡單多了。這里筆者就不把全部的過程拿出來講了。就直接把獲得ParameterMapping類的始點提出來——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()); }
ParameterMappingTokenHandler類就是用於解析ParameterMapping類的。具體如何去解析不用筆者在動手吧。在建新ParameterMapping類的時候,他的內部resolveTypeHandler方法會被調用。而這個時候我們的類型處理器正式出現了。
private void resolveTypeHandler() { if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) { Configuration configuration = parameterMapping.configuration; TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType); } }
類型處理器相關的內容。到了這里可以說是結束了。我們知道了他的來源,也知道了他的去處。事實如果我們認真點去了解正個過程。你們會發現這個過程用於很buidler設計模式。同時mybatis框架把mapper文件的信息分為mapper根處理、四節點處理、腳本處理、SQL語句處理。
1.Mapper根處理對應的XMLMapperBuilder類。
2.四節點處理對應的XMLStatementBuilder類。即是select節點,update節點,insert節點,delect節點。
3.腳本處理對應的XMLScriptBuilder類。
4.SQL語句處理對應的SqlSourceBuilder類。
當然這也是筆者自己的看法而以。不一定對。你們可以自己去了解。