深刻了解MyBatis參數

深刻了解MyBatis參數

 

 

深刻了解MyBatis參數

相信不少人可能都遇到過下面這些異常: java

  • "Parameter 'xxx' not found. Available parameters are [...]" git

  • "Could not get property 'xxx' from xxxClass. Cause: github

  • "The expression 'xxx' evaluated to a null value." sql

  • "Error evaluating expression 'xxx'. Return value (xxxxx) was not iterable." express

不僅是上面提到的這幾個,我認爲有不少的錯誤都產生在和參數有關的地方。 數組

想要避免參數引發的錯誤,咱們須要深刻了解參數。 mybatis

想了解參數,咱們首先看MyBatis處理參數和使用參數的所有過程。 app

本篇因爲爲了便於理解和深刻,使用了大量的源碼,所以篇幅較長,須要必定的耐心看完,本文必定會對你起到很大的幫助。 ide

參數處理過程

處理接口形式的入參

在使用MyBatis時,有兩種使用方法。一種是使用的接口形式,另外一種是經過SqlSession調用命名空間。這兩種方式在傳遞參數時是不同的,命名空間的方式更直接,可是多個參數時須要咱們本身建立Map做爲入參。相比而言,使用接口形式更簡單。

接口形式的參數是由MyBatis本身處理的。若是使用接口調用,入參須要通過額外的步驟處理入參,以後就和命名空間方式同樣了。

MapperMethod.java會首先通過下面方法來轉換參數:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 指定了名字,就會記錄這個名字,若是沒有記錄,那麼就會使用它的序號做爲名字。

例若有以下接口:

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

那麼他對應的params以下:

?
1
2
3
4
{
     0: 'sex' ,
     1: '1'
}

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

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

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

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

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

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

處理集合

不論是selectOne仍是selectMap方法,歸根結底都是經過selectList進行查詢的,不論是delete仍是insert方法,都是經過update方法操做的。在selectList和update中全部參數的都進行了統一的處理。

DefaultSqlSession.java中的wrapCollection方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>中默認狀況下寫的arraylist(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中,從構造方法看起:

?
1
2
3
4
5
6
7
8
9
10
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就是咱們通過前面兩步處理後的參數。這個參數通過前面兩步處理後,到這裏的時候,他只有下面三種狀況:

  1. null,若是沒有入參或者入參是null,到這裏也是null。
  2. Map類型,除了null以外,前面兩步主要是封裝成Map類型。
  3. 數組、集合和Map之外的Object類型,能夠是基本類型或者實體類。

看上面構造方法,若是參數是1,2狀況時,執行代碼bindings = new ContextMap(null);參數是3狀況時執行if中的代碼。咱們看看ContextMap類,這是一個內部靜態類,代碼以下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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以後還有兩行:

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

其中兩個Key分別爲:

?
1
2
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:

?
1
2
3
static {
   OgnlRuntime.setPropertyAccessor(ContextMap. class , new ContextAccessor());
}

當從ContextMap取值的時候,會執行ContextAccessor中的以下方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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中的屬性名。

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

  • 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());

    name屬性是什麼呢,有什麼可選值呢?這就是處理接口形式的入參處理集合處理後所擁有的Key。
    若是你遇到過相似異常,相信看到這兒就明白緣由了。

  • 數組、集合和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方法:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
@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的方式,例若有以下一個碼值類:

 

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

若是在xml,能夠這麼使用:

 

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

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

#{propertyName}參數

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

DefaultParameterHandler.java中:

 

?
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
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判斷的地方,單獨拿出來分析:

 

?
1
2
3
4
5
6
7
8
9
10
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>彷佛是遇到問題最多的一個。

例若有下面的方法:

?
1
2
3
4
5
6
7
< 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方法中的前兩行:

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

這裏的bindings參數熟悉嗎?上面提到過不少。通過一系列的參數處理後,這兒的bindings以下:

?
1
2
3
4
5
6
7
{
   "_parameter" :{
     "param1" :list,
     "userList" :list
   },
   "_databaseId" : null ,
}

collectionExpression就是collection="userList"的值userList。

咱們看看evaluator.evaluateIterable如何處理這個參數,在ExpressionEvaluator.java中的evaluateIterable方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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." );
}

首先經過看第一行代碼:

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

這裏經過OGNL獲取到了userList的值。獲取userList值的時候可能出現異常,具體能夠參考上面動態SQL部分的內容。

userList的值分四種狀況。

  1. value == null,這種狀況直接拋出異常BuilderException。

  2. value instanceof Iterable,實現Iterable接口的直接返回,如Collection的全部子類,一般是List。

  3. value.getClass().isArray()數組的狀況,這種狀況會轉換爲List返回。

  4. value instanceof Map若是是Map,經過((Map) value).entrySet()返回一個Set類型的參數。

經過上面處理後,返回的值,是一個Iterable類型的值,這個值可使用for (Object o : iterable)這種形式循環。

在ForEachSqlNode中對iterable循環的時候,有一段須要關注的代碼:

?
1
2
3
4
5
6
7
8
9
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中。

最後

這篇文章很長,寫這篇文章耗費的時間也很長,超過10小時,寫到半夜兩點都沒寫完。

這篇文章真的很是有用,若是你對Mybatis有必定的瞭解,這篇文章幾乎是必讀的一篇。

若是各位發現文中錯誤或者其餘問題歡迎留言或加羣詳談。

MyBatis分頁插件

http://git.oschina.net/free/Mybatis_PageHelper

MyBatis通用Mapper

http://git.oschina.net/free/Mapper

Mybatis專欄:


  1. 這裏的Map實際類型爲ParamMap<V>,和下一步處理集合中的StrictMap<V>類是兩個功能徹底同樣的類。
  2. 這裏的Map實際類型爲StrictMap<V>,和接口處理中的ParamMap<V>類是兩個功能徹底同樣的類。
相關文章
相關標籤/搜索