程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深入學習MyBatis中的參數(推薦)

深入學習MyBatis中的參數(推薦)

編輯:關於JAVA

前言

相信很多人可能都遇到過下面這些異常:

"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<Object>();
 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;
 }
}

在這裡有個很關鍵的params,這個參數類型為Map<Integer, String> ,他會根據接口方法按順序記錄下接口參數的定義的名字,如果使用@Param指定了名字,就會記錄這個名字,如果沒有記錄,那麼就會使用它的序號作為名字。

例如有如下接口:

List<User> select(@Param('sex')String sex,Integer age);

那麼他對應的params如下:

{
 0:'sex',
 1:'1'
}

繼續看上面的convertArgsToSqlCommandParam方法,這裡簡要說明3種情況:

入參為null或沒有時,參數轉換為null 沒有使用@Param注解並且只有一個參數時,返回這一個參數 使用了@Param注解或有多個參數時,將參數轉換為Map1類型,並且還根據參數順序存儲了key為param1,param2的參數。

注意:從第3種情況來看,建議各位有多個入參的時候通過@Param指定參數名,方便後面(動態sql)的使用。

經過上面方法的處理後,在MapperMethod中會繼續往下調用命名空間方式的方法:

Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.<E>selectList(command.getName(), param);

從這之後開始按照統一的方式繼續處理入參。

處理集合

不管是selectOne還是selectMap方法,歸根結底都是通過selectList進行查詢的,不管是delete還是insert方法,都是通過update方法操作的。在selectList和update中所有參數的都進行了統一的處理。

在DefaultSqlSession.java中的wrapCollection方法:

private Object wrapCollection(final Object object) {
 if (object instanceof Collection) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("collection", object);
 if (object instanceof List) {
 map.put("list", object);
 }
 return map; 
 } else if (object != null && object.getClass().isArray()) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("array", object);
 return map;
 }
 return object;
}

這裡特別需要注意的一個地方是map.put("collection", object) ,這個設計是為了支持Set類型,需要等到MyBatis 3.3.0版本才能使用。

wrapCollection處理的是只有一個參數時,集合和數組的類型轉換成Map2類型,並且有默認的Key,從這裡你能大概看到為什麼<foreach>中默認情況下寫的array和list(Map類型沒有默認值map)。

參數的使用

參數的使用分為兩部分:

第一種就是常見#{username}或者${username} 。 第二種就是在動態SQL中作為條件,例如<if test="username!=null and username !=''">

下面對這兩種進行詳細講解,為了方便理解,先講解第二種情況。

在動態SQL條件中使用參數

關於動態SQL的基礎內容可以查看官方文檔。

動態SQL為什麼會處理參數呢?

主要是因為動態SQL中的<if> , <bind> , <foreache>都會用到表達式,表達式中會用到屬性名,屬性名對應的屬性值如何獲取呢?獲取方式就在這關鍵的一步。不知道多少人遇到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());
}

這裡的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;
 }
}

我們先繼續看DynamicContext的構造方法,在if/else之後還有兩行:

bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());

其中兩個Key分別為:

public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";

也就是說1,2兩種情況的時候,參數值只存在於"_parameter"的鍵值中。3情況的時候,參數值存在於"_parameter"的鍵值中,也存在於bindings本身。

當動態SQL取值的時候會通過OGNL從bindings中獲取值。MyBatis在OGNL中注冊了ContextMap:

static {
 OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}

當從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;
}

參數中的target就是ContextMap類型的,所以可以直接強轉為Map類型。

參數中的name就是我們寫在動態SQL中的屬性名。

下面舉例說明這三種情況:

1、null的時候:

不管name是什麼(name="_databaseId"除外,可能會有值),此時Object result = map.get(name);得到的result=null

Object parameterObject = map.get(PARAMETER_OBJECT_KEY);parameterObject=null,因此最後返回的結果是null。

在這種情況下,不管寫什麼樣的屬性,值都會是null,並且不管屬性是否存在,都不會出錯。

2、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());

name屬性是什麼呢,有什麼可選值呢?這就是處理接口形式的入參和處理集合處理後所擁有的Key。

如果你遇到過類似異常,相信看到這兒就明白原因了。

3、數組、集合和Map以外的Object類型:

這種類型經過了下面的處理:

MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);

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;
}

可以看到這裡會優先從Map中取該屬性的值,如果不存在,那麼一定會執行到下面這行代碼:

return parameterMetaObject.getValue(strKey)

如果name剛好是對象的一個屬性值,那麼通過MetaObject反射可以獲取該屬性值。如果該對象不包含name屬性的值,就會報錯:

throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);

理解這三種情況後,使用動態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;
}

put("value"這個地方可以看出來,MyBatis會創建一個默認為"value"的值,也就是說,在xml中的SQL中可以直接使用${value},從else if可以看出來,只有是簡單類型的時候,才會有值。

關於這點,舉個簡單例子,如果接口為List<User> selectOrderby(String column) ,如果xml內容為:

<select id="selectOrderby" resultType="User">
select * from user order by ${value}
</select>

這種情況下,雖然沒有指定一個value屬性,但是MyBatis會自動把參數column賦值進去。

再往下的代碼:

Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));

這裡和動態SQL就一樣了,通過OGNL方式來獲取值。

看到這裡使用OGNL這種方式時,你有沒有別的想法?

特殊用法:你是否在SQL查詢中使用過某些固定的碼值?一旦碼值改變的時候需要改動很多地方,但是你又不想把碼值作為參數傳進來,怎麼解決呢?你可能已經明白了。

就是通過OGNL的方式,例如有如下一個碼值類:

package com.abel533.mybatis;
public interface Code{
 public static final String ENABLE = "1";
 public static final String DISABLE = "0";
}

如果在xml,可以這麼使用:

<select id="selectUser" resultType="User">
 select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE}
</select>

除了碼值之外,你可以使用OGNL支持的各種方法,如調用靜態方法。

#{propertyName}參數

這種方式比較簡單,復雜屬性的時候使用的MyBatis的MetaObject。

在DefaultParameterHandler.java中:

public void setParameters(PreparedStatement ps) throws SQLException {
 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();
  }
  typeHandler.setParameter(ps, i + 1, value, jdbcType);
  }
 }
 }
}

上面這段代碼就是從參數中取#{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);
}

首先看第一個if,當使用<foreach>的時候,MyBatis會自動生成額外的動態參數,如果propertyName是動態參數,就會從動態參數中取值。

第二個if,如果參數是null,不管屬性名是什麼,都會返回null。

第三個if,如果參數是一個簡單類型,或者是一個注冊了typeHandler的對象類型,就會直接使用該參數作為返回值,和屬性名無關。

最後一個else,這種情況下是復雜對象或者Map類型,通過反射方便的取值。

下面我們說明上面四種情況下的參數名注意事項。

1、動態參數,這裡的參數名和值都由MyBatis動態生成的,因此我們沒法直接接觸,也不需要管這兒的命名。但是我們可以了解一下這兒的命名規則,當以後錯誤信息看到的時候,我們可以確定出錯的地方。
在ForEachSqlNode.java中:

private static String itemizeItem(String item, int i) {
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}

其中ITEM_PRFIX為public static final String ITEM_PREFIX = "__frch_";

如果在<foreach>中的collection="userList" item="user" , 那麼對userList循環產生的動態參數名就是:

__frch_user_0,__frch_user_1,__frch_user_2…

如果訪問動態參數的屬性,如user.username會被處理成__frch_user_0.username ,這種參數值的處理過程在更早之前解析SQL的時候就已經獲取了對應的參數值。具體內容看下面有關<foreach>的詳細內容。

2、參數為null,由於這裡的判斷和參數名無關,因此入參null的時候,在xml中寫的#{name}不管name寫什麼,都不會出錯,值都是null。

3、可以直接使用typeHandler處理的類型。最常見的就是基本類型,例如有這樣一個接口方法User selectById(@Param("id")Integer id) ,在xml中使用id的時候,我們可以隨便使用屬性名,不管用什麼樣的屬性名,值都是id。

4、復雜對象或者Map類型一般都是我們需要注意的地方,這種情況下,就必須保證入參包含這些屬性,如果沒有就會報錯。這一點和可以參考上面有關MetaObject的地方。

<foreach>詳解

所有動態SQL類型中, <foreach>似乎是遇到問題最多的一個。

例如有下面的方法:

<insert id="insertUserList">
 INSERT INTO user(username,password)
 VALUES
 <foreach collection="userList" item="user" separator=",">
 (#{user.username},#{user.password})
 </foreach>
</insert>

對應的接口:

int insertUserList(@Param("userList")List<User> list);

我們通過foreach源碼,看看MyBatis如何處理上面這個例子。

在ForEachSqlNode.java中的apply方法中的前兩行:

Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);

這裡的bindings參數熟悉嗎?上面提到過很多。經過一系列的參數處理後,這兒的bindings如下:

{
 "_parameter":{
 "param1":list,
 "userList":list
 },
 "_databaseId":null,
}

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<Object> answer = new ArrayList<Object>();
  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.");
}

首先通過看第一行代碼:

Object value = OgnlCache.getValue(expression, parameterObject);

這裡通過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);
}

如果是通過((Map) value).entrySet()返回的Set,那麼循環取得的子元素都是Map.Entry類型,這個時候會將mapEntry.getKey()存儲到index中,mapEntry.getValue()存儲到item中。

如果是List,那麼會將序號i存到index中,mapEntry.getValue()存儲到item中。

<foreach>常見錯誤補充

collection="userList"的值userList中的User是一個繼承自Map的類型時,你需要保證<foreach>循環中用到的所有對象的屬性必須存在,Map類型存在的問題通常是,如果某個值是null,一般是不存在相應的key,這種情況會導致<foreach>出錯,會報找不到__frch_user_x參數。所以這種情況下,就是值是null,你也需要map.put(key,null)

總結

以上就是這篇文章的全部內容了,這篇文章真的非常有用,如果你對Mybatis有一定的了解,這篇文章幾乎是必讀的一篇。希望本文的內容對大家的學習或者工作能帶來一定的幫助,如有疑問大家可以留言交流,謝謝大家對的支持。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved