深入瞭解mybatis引數
深入瞭解MyBatis引數 相信很多人可能都遇到過下面這些異常:
“Parameter ‘xxx’ not found. Available parameters are […]”
"Could not get property ‘xxx’ from xxxClass. Cause:
“The expression ‘xxx’ evaluated to a null value.”
“Error evaluating expression ‘xxx’. Return value (xxxxx) was not iterable.”
不只是上面提到的這幾個,我認為有很多的錯誤都產生在和引數有關的地方。
想要避免參數引起的錯誤,我們需要深入瞭解引數。
想了解引數,我們首先看MyBatis處理引數和使用引數的全部過程。
本篇由於為了便於理解和深入,使用了大量的原始碼,因此篇幅較長,需要一定的耐心看完,本文一定會對你起到很大的幫助。
引數處理過程 處理介面形式的入參 在使用MyBatis時,有兩種使用方法。一種是使用的介面形式,另一種是通過SqlSession呼叫名稱空間。這兩種方式在傳遞引數時是不一樣的,名稱空間的方式更直接,但是多個引數時需要我們自己建立Map作為入參。相比而言,使用介面形式更簡單。
介面形式的引數是由MyBatis自己處理的。如果使用介面呼叫,入參需要經過額外的步驟處理入參,之後就和名稱空間方式一樣了。
在MapperMethod.java會首先經過下面方法來轉換引數:
public Object convertArgsToSqlCommandParam(Object[] args) { final int paramCount = params.size(); if (args == null || paramCount == 0) { return null; } else if (!hasNamedParameters && paramCount == 1) { return args[params.keySet().iterator().next()]; } else { final Map<String, Object> param = new ParamMap(); int i = 0; for (Map.Entry<Integer, String> entry : params.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // issue #71, add param names as param1, param2…but ensure backward compatibility final String genericParamName = “param” + String.valueOf(i + 1); if (!param.containsKey(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 在這裡有個很關鍵的params,這個引數型別為Map<Integer, String>,他會根據介面方法按順序記錄下介面引數的定義的名字,如果使用@Param指定了名字,就會記錄這個名字,如果沒有記錄,那麼就會使用它的序號作為名字。
例如有如下介面:
List select(@Param(‘sex’)String sex,Integer age); 1 那麼他對應的params如下:
{ 0:‘sex’, 1:‘1’ } 1 2 3 4 繼續看上面的convertArgsToSqlCommandParam方法,這裡簡要說明3種情況:
入參為null或沒有時,引數轉換為null 沒有使用@Param註解並且只有一個引數時,返回這一個引數 使用了@Param註解或有多個引數時,將引數轉換為Map1型別,並且還根據引數順序儲存了key為param1,param2的引數。 注意:從第3種情況來看,建議各位有多個入參的時候通過@Param指定引數名,方便後面(動態sql)的使用。
經過上面方法的處理後,在MapperMethod中會繼續往下呼叫名稱空間方式的方法:
Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectList(command.getName(), param); 1 2 從這之後開始按照統一的方式繼續處理入參。
處理集合 不管是selectOne還是selectMap方法,歸根結底都是通過selectList進行查詢的,不管是delete還是insert方法,都是通過update方法操作的。在selectList和update中所有引數的都進行了統一的處理。
在DefaultSqlSession.java中的wrapCollection方法:
private Object wrapCollection(final Object object) { if (object instanceof Collection) { StrictMap map = new StrictMap(); map.put(“collection”, object); if (object instanceof List) { map.put(“list”, object); } return map; } else if (object != null && object.getClass().isArray()) { StrictMap map = new StrictMap(); map.put(“array”, object); return map; } return object; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 這裡特別需要注意的一個地方是map.put(“collection”, object),這個設計是為了支援Set型別,需要等到MyBatis 3.3.0版本才能使用。
wrapCollection處理的是隻有一個引數時,集合和陣列的型別轉換成Map2型別,並且有預設的Key,從這裡你能大概看到為什麼中預設情況下寫的array和list(Map型別沒有預設值map)。
引數的使用 引數的使用分為兩部分:
第一種就是常見#{username}或者${username}。 第二種就是在動態SQL中作為條件,例如。 下面對這兩種進行詳細講解,為了方便理解,先講解第二種情況。
在動態SQL條件中使用引數 關於動態SQL的基礎內容可以檢視官方文件。
動態SQL為什麼會處理引數呢?
主要是因為動態SQL中的,,都會用到表示式,表示式中會用到屬性名,屬性名對應的屬性值如何獲取呢?獲取方式就在這關鍵的一步。不知道多少人遇到Could not get property xxx from xxxClass或: Parameter ‘xxx’ not found. Available parameters are[…],都是不懂這裡引起的。
在DynamicContext.java中,從構造方法看起:
public DynamicContext(Configuration configuration, Object parameterObject) { if (parameterObject != null && !(parameterObject instanceof Map)) { MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject); } else { bindings = new ContextMap(null); } bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); } 1 2 3 4 5 6 7 8 9 10 這裡的Object parameterObject就是我們經過前面兩步處理後的引數。這個引數經過前面兩步處理後,到這裡的時候,他只有下面三種情況:
null,如果沒有入參或者入參是null,到這裡也是null。 Map型別,除了null之外,前面兩步主要是封裝成Map型別。 陣列、集合和Map以外的Object型別,可以是基本型別或者實體類。 看上面構造方法,如果引數是1,2情況時,執行程式碼bindings = new ContextMap(null);引數是3情況時執行if中的程式碼。我們看看ContextMap類,這是一個內部靜態類,程式碼如下:
static class ContextMap extends HashMap<String, Object> { private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; } public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject != null) { // issue #61 do not modify the context when reading return parameterMetaObject.getValue(strKey); } return null; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 我們先繼續看DynamicContext的構造方法,在if/else之後還有兩行:
bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); 1 2 其中兩個Key分別為:
public static final String PARAMETER_OBJECT_KEY = “_parameter”; public static final String DATABASE_ID_KEY = “_databaseId”; 1 2 也就是說1,2兩種情況的時候,引數值只存在於"_parameter"的鍵值中。3情況的時候,引數值存在於"_parameter"的鍵值中,也存在於bindings本身。
當動態SQL取值的時候會通過OGNL從bindings中獲取值。MyBatis在OGNL中註冊了ContextMap:
static { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } 1 2 3 當從ContextMap取值的時候,會執行ContextAccessor中的如下方法:
@Override public Object getProperty(Map context, Object target, Object name) throws OgnlException { Map map = (Map) target;
Object result = map.get(name); if (map.containsKey(name) || result != null) { return result; }
Object parameterObject = map.get(PARAMETER_OBJECT_KEY); if (parameterObject instanceof Map) { return ((Map)parameterObject).get(name); }
return null; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 引數中的target就是ContextMap型別的,所以可以直接強轉為Map型別。 引數中的name就是我們寫在動態SQL中的屬性名。
下面舉例說明這三種情況:
null的時候: 不管name是什麼(name="_databaseId"除外,可能會有值),此時Object result = map.get(name);得到的result=null。 在Object parameterObject = map.get(PARAMETER_OBJECT_KEY);中parameterObject=null,因此最後返回的結果是null。 在這種情況下,不管寫什麼樣的屬性,值都會是null,並且不管屬性是否存在,都不會出錯。
Map型別: 此時Object result = map.get(name);一般也不會有值,因為引數值只存在於"_parameter"的鍵值中。 然後到Object parameterObject = map.get(PARAMETER_OBJECT_KEY);,此時獲取到我們的引數值。 在從引數值((Map)parameterObject).get(name)根據name來獲取屬性值。 在這一步的時候,如果name屬性不存在,就會報錯:
throw new BindingException(“Parameter '” + key + "’ not found. Available parameters are " + keySet()); 1 name屬性是什麼呢,有什麼可選值呢?這就是處理介面形式的入參和處理集合處理後所擁有的Key。 如果你遇到過類似異常,相信看到這兒就明白原因了。
陣列、集合和Map以外的Object型別: 這種型別經過了下面的處理:
MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject); 1 2 MetaObject是MyBatis的一個反射類,可以很方便的通過getValue方法獲取物件的各種屬性(支援集合陣列和Map,可以多級屬性點.訪問,如user.username,user.roles[1].rolename)。 現在分析這種情況。 首先通過name獲取屬性時Object result = map.get(name);,根據上面ContextMap類中的get方法:
public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject != null) { return parameterMetaObject.getValue(strKey); } return null; } 1 2 3 4 5 6 7 8 9 10 可以看到這裡會優先從Map中取該屬性的值,如果不存在,那麼一定會執行到下面這行程式碼:
return parameterMetaObject.getValue(strKey) 1 如果name剛好是物件的一個屬性值,那麼通過MetaObject反射可以獲取該屬性值。如果該物件不包含name屬性的值,就會報錯:
throw new ReflectionException(“Could not get property '” + prop.getName() + "’ from " + object.getClass() + ". Cause: " + t.toString(), t); 1 理解這三種情況後,使用動態SQL應該不會有引數名方面的問題了。
在SQL語句中使用引數 SQL中的兩種形式#{username}或者${username},雖然看著差不多,但是實際處理過程差別很大,而且很容易出現莫名其妙的錯誤。
${username}的使用方式為OGNL方式獲取值,和上面的動態SQL一樣,這裡先說這種情況。
${propertyName}引數 在TextSqlNode.java中有一個內部的靜態類BindingTokenParser,現在只看其中的handleToken方法:
@Override 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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 從put("value"這個地方可以看出來,MyBatis會建立一個預設為"value"的值,也就是說,在xml中的SQL中可以直接使用${value},從else if可以看出來,只有是簡單型別的時候,才會有值。
關於這點,舉個簡單例子,如果介面為List selectOrderby(String column),如果xml內容為:
select * from user order by ${value} 1 2 3 這種情況下,雖然沒有指定一個value屬性,但是MyBatis會自動把引數column賦值進去。再往下的程式碼:
Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = (value == null ? “” : String.valueOf(value)); 1 2 這裡和動態SQL就一樣了,通過OGNL方式來獲取值。
看到這裡使用OGNL這種方式時,你有沒有別的想法? 特殊用法:你是否在SQL查詢中使用過某些固定的碼值?一旦碼值改變的時候需要改動很多地方,但是你又不想把碼值作為引數傳進來,怎麼解決呢?你可能已經明白了。 就是通過OGNL的方式,例如有如下一個碼值類:
package com.abel533.mybatis; public interface Code{ public static final String ENABLE = “1”; public static final String DISABLE = “0”; } 1 2 3 4 5 如果在xml,可以這麼使用:
select * from user where enable = ${@[email protected]} 1 2 3 除了碼值之外,你可以使用OGNL支援的各種方法,如呼叫靜態方法。#{propertyName}引數 這種方式比較簡單,複雜屬性的時候使用的MyBatis的MetaObject。
在DefaultParameterHandler.java中:
public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity(“setting parameters”).object(mappedStatement.getParameterMap().getId()); List 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(); } typeHandler.setParameter(ps, i + 1, value, jdbcType); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 上面這段程式碼就是從引數中取#{propertyName}值的方法,這段程式碼的主要邏輯就是if/else判斷的地方,單獨拿出來分析:
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); } 1 2 3 4 5 6 7 8 9 10 首先看第一個if,當使用的時候,MyBatis會自動生成額外的動態引數,如果propertyName是動態引數,就會從動態引數中取值。 第二個if,如果引數是null,不管屬性名是什麼,都會返回null。 第三個if,如果引數是一個簡單型別,或者是一個註冊了typeHandler的物件型別,就會直接使用該引數作為返回值,和屬性名無關。 最後一個else,這種情況下是複雜物件或者Map型別,通過反射方便的取值。 下面我們說明上面四種情況下的引數名注意事項。
動態引數,這裡的引數名和值都由MyBatis動態生成的,因此我們沒法直接接觸,也不需要管這兒的命名。但是我們可以瞭解一下這兒的命名規則,當以後錯誤資訊看到的時候,我們可以確定出錯的地方。 在ForEachSqlNode.java中:
private static String itemizeItem(String item, int i) { return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString(); } 1 2 3 其中ITEM_PRFIX為public static final String ITEM_PREFIX = “_frch”;。 如果在中的collection=“userList” item=“user”,那麼對userList迴圈產生的動態引數名就是:
__frch_user_0,__frch_user_1,__frch_user_2…
如果訪問動態引數的屬性,如user.username會被處理成__frch_user_0.username,這種引數值的處理過程在更早之前解析SQL的時候就已經獲取了對應的引數值。具體內容看下面有關的詳細內容。
引數為null,由於這裡的判斷和引數名無關,因此入參null的時候,在xml中寫的#{name}不管name寫什麼,都不會出錯,值都是null。
可以直接使用typeHandler處理的型別。最常見的就是基本型別,例如有這樣一個介面方法User selectById(@Param(“id”)Integer id),在xml中使用id的時候,我們可以隨便使用屬性名,不管用什麼樣的屬性名,值都是id。
複雜物件或者Map型別一般都是我們需要注意的地方,這種情況下,就必須保證入參包含這些屬性,如果沒有就會報錯。這一點和可以參考上面有關MetaObject的地方。
詳解 所有動態SQL型別中,似乎是遇到問題最多的一個。
例如有下面的方法:
INSERT INTO user(username,password) VALUES (#{user.username},#{user.password}) 1 2 3 4 5 6 7 對應的介面:int insertUserList(@Param(“userList”)List list); 1 我們通過foreach原始碼,看看MyBatis如何處理上面這個例子。
在ForEachSqlNode.java中的apply方法中的前兩行:
Map<String, Object> bindings = context.getBindings(); final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); 1 2 這裡的bindings引數熟悉嗎?上面提到過很多。經過一系列的引數處理後,這兒的bindings如下:
{ “_parameter”:{ “param1”:list, “userList”:list }, “_databaseId”:null, } 1 2 3 4 5 6 7 collectionExpression就是collection="userList"的值userList。
我們看看evaluator.evaluateIterable如何處理這個引數,在ExpressionEvaluator.java中的evaluateIterable方法:
public Iterable<?> evaluateIterable(String expression, Object parameterObject) { Object value = OgnlCache.getValue(expression, parameterObject); if (value == null) { throw new BuilderException(“The expression '” + expression + “’ evaluated to a null value.”); } if (value instanceof Iterable) { return (Iterable<?>) value; } if (value.getClass().isArray()) { int size = Array.getLength(value); List answer = new ArrayList(); for (int i = 0; i < size; i++) { Object o = Array.get(value, i); answer.add(o); } return answer; } if (value instanceof Map) { return ((Map) value).entrySet(); } throw new BuilderException(“Error evaluating expression '” + expression + “’. Return value (” + value + “) was not iterable.”); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 首先通過看第一行程式碼:
Object value = OgnlCache.getValue(expression, parameterObject); 1 這裡通過OGNL獲取到了userList的值。獲取userList值的時候可能出現異常,具體可以參考上面動態SQL部分的內容。
userList的值分四種情況。
value == null,這種情況直接丟擲異常BuilderException。
value instanceof Iterable,實現Iterable介面的直接返回,如Collection的所有子類,通常是List。
value.getClass().isArray()陣列的情況,這種情況會轉換為List返回。
value instanceof Map如果是Map,通過((Map) value).entrySet()返回一個Set型別的引數。
通過上面處理後,返回的值,是一個Iterable型別的值,這個值可以使用for (Object o : iterable)這種形式迴圈。
在ForEachSqlNode中對iterable迴圈的時候,有一段需要關注的程式碼:
if (o instanceof Map.Entry) { @SuppressWarnings(“unchecked”) Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { applyIndex(context, i, uniqueNumber); applyItem(context, o, uniqueNumber); } 1 2 3 4 5 6 7 8 9 如果是通過((Map) value).entrySet()返回的Set,那麼迴圈取得的子元素都是Map.Entry型別,這個時候會將mapEntry.getKey()儲存到index中,mapEntry.getValue()儲存到item中。
如果是List,那麼會將序號i存到index中,mapEntry.getValue()儲存到item中。
常見錯誤補充 當collection="userList"的值userList中的User是一個繼承自Map的型別時,你需要保證迴圈中用到的所有物件的屬性必須存在,Map型別存在的問題通常是,如果某個值是null,一般是不存在相應的key,這種情況會導致出錯,會報找不到__frch_user_x引數。所以這種情況下,就是值是null,你也需要map.put(key,null)。
最後 這篇文章很長,寫這篇文章耗費的時間也很長,超過10小時,寫到半夜兩點都沒寫完。
這篇文章真的非常有用,如果你對Mybatis有一定的瞭解,這篇文章幾乎是必讀的一篇。
如果各位發現文中錯誤或者其他問題歡迎留言或加群詳談。
Mybatis專欄: Mybatis示例
Mybatis問題集