深入了解mybatis參數(shù)
相信很多人可能都遇到過下面這些異常:
- "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."
不只是上面提到的這幾個,我認為有很多的錯誤都產(chǎn)生在和參數(shù)有關(guān)的地方。
想要避免參數(shù)引起的錯誤,我們需要深入了解參數(shù)。
想了解參數(shù),我們首先看mybatis處理參數(shù)和使用參數(shù)的全部過程。
本篇由于為了便于理解和深入,使用了大量的源碼,因此篇幅較長,需要一定的耐心看完,本文一定會對你起到很大的幫助。
參數(shù)處理過程
處理接口形式的入?yún)?/strong>
在使用mybatis時,有兩種使用方法。一種是使用的接口形式,另一種是通過sqlsession調(diào)用命名空間。這兩種方式在傳遞參數(shù)時是不一樣的,命名空間的方式更直接,但是多個參數(shù)時需要我們自己創(chuàng)建map作為入?yún)?。相比而言,使用接口形式更簡單?/p>
接口形式的參數(shù)是由mybatis自己處理的。如果使用接口調(diào)用,入?yún)⑿枰?jīng)過額外的步驟處理入?yún)?,之后就和命名空間方式一樣了。
在mappermethod.java會首先經(jīng)過下面方法來轉(zhuǎn)換參數(shù):
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; } } |
在這里有個很關(guān)鍵的params,這個參數(shù)類型為map<integer, string>,他會根據(jù)接口方法按順序記錄下接口參數(shù)的定義的名字,如果使用@param指定了名字,就會記錄這個名字,如果沒有記錄,那么就會使用它的序號作為名字。
例如有如下接口:
1
|
list<user> select( @param ( 'sex' )string sex,integer age); |
那么他對應(yīng)的params如下:
1
2
3
4
|
{ 0 : 'sex' , 1 : '1' } |
繼續(xù)看上面的convertargstosqlcommandparam方法,這里簡要說明3種情況:
- 入?yún)閚ull或沒有時,參數(shù)轉(zhuǎn)換為null
- 沒有使用@param注解并且只有一個參數(shù)時,返回這一個參數(shù)
- 使用了@param注解或有多個參數(shù)時,將參數(shù)轉(zhuǎn)換為map1類型,并且還根據(jù)參數(shù)順序存儲了key為param1,param2的參數(shù)。
注意:從第3種情況來看,建議各位有多個入?yún)⒌臅r候通過@param指定參數(shù)名,方便后面(動態(tài)sql)的使用。
經(jīng)過上面方法的處理后,在mappermethod中會繼續(xù)往下調(diào)用命名空間方式的方法:
1
2
|
object param = method.convertargstosqlcommandparam(args); result = sqlsession.<e>selectlist(command.getname(), param); |
從這之后開始按照統(tǒng)一的方式繼續(xù)處理入?yún)ⅰ?/p>
處理集合
不管是selectone還是selectmap方法,歸根結(jié)底都是通過selectlist進行查詢的,不管是delete還是insert方法,都是通過update方法操作的。在selectlist和update中所有參數(shù)的都進行了統(tǒng)一的處理。
在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),這個設(shè)計是為了支持set類型,需要等到mybatis 3.3.0版本才能使用。
wrapcollection處理的是只有一個參數(shù)時,集合和數(shù)組的類型轉(zhuǎn)換成map2類型,并且有默認的key,從這里你能大概看到為什么<foreach>中默認情況下寫的array和list(map類型沒有默認值map)。
參數(shù)的使用
參數(shù)的使用分為兩部分:
- 第一種就是常見#{username}或者${username}。
- 第二種就是在動態(tài)sql中作為條件,例如<if test="username!=null and username !=''">。
下面對這兩種進行詳細講解,為了方便理解,先講解第二種情況。
在動態(tài)sql條件中使用參數(shù)
關(guān)于動態(tài)sql的基礎(chǔ)內(nèi)容可以查看官方文檔。
動態(tài)sql為什么會處理參數(shù)呢?
主要是因為動態(tài)sql中的<if>,<bind>,<foreache>都會用到表達式,表達式中會用到屬性名,屬性名對應(yīng)的屬性值如何獲取呢?獲取方式就在這關(guān)鍵的一步。不知道多少人遇到could not get property xxx from xxxclass或: parameter ‘xxx' not found. available parameters are[…],都是不懂這里引起的。
在dynamiccontext.java中,從構(gòu)造方法看起:
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就是我們經(jīng)過前面兩步處理后的參數(shù)。這個參數(shù)經(jīng)過前面兩步處理后,到這里的時候,他只有下面三種情況:
- null,如果沒有入?yún)⒒蛘呷雲(yún)⑹莕ull,到這里也是null。
- map類型,除了null之外,前面兩步主要是封裝成map類型。
- 數(shù)組、集合和map以外的object類型,可以是基本類型或者實體類。
看上面構(gòu)造方法,如果參數(shù)是1,2情況時,執(zhí)行代碼bindings = new contextmap(null);參數(shù)是3情況時執(zhí)行if中的代碼。我們看看contextmap類,這是一個內(nèi)部靜態(tài)類,代碼如下:
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 ; } } |
我們先繼續(xù)看dynamiccontext的構(gòu)造方法,在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兩種情況的時候,參數(shù)值只存在于"_parameter"的鍵值中。3情況的時候,參數(shù)值存在于"_parameter"的鍵值中,也存在于bindings本身。
當(dāng)動態(tài)sql取值的時候會通過ognl從bindings中獲取值。mybatis在ognl中注冊了contextmap:
1
2
3
|
static { ognlruntime.setpropertyaccessor(contextmap. class , new contextaccessor()); } |
當(dāng)從contextmap取值的時候,會執(zhí)行contextaccessor中的如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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 ; } |
參數(shù)中的target就是contextmap類型的,所以可以直接強轉(zhuǎn)為map類型。
參數(shù)中的name就是我們寫在動態(tài)sql中的屬性名。
下面舉例說明這三種情況:
null的時候:
不管name是什么(name="_databaseid"除外,可能會有值),此時object result = map.get(name);得到的result=null。
在object parameterobject = map.get(parameter_object_key);中parameterobject=null,因此最后返回的結(jié)果是null。
在這種情況下,不管寫什么樣的屬性,值都會是null,并且不管屬性是否存在,都不會出錯。
map類型:
此時object result = map.get(name);一般也不會有值,因為參數(shù)值只存在于"_parameter"的鍵值中。
然后到object parameterobject = map.get(parameter_object_key);,此時獲取到我們的參數(shù)值。
在從參數(shù)值((map)parameterobject).get(name)根據(jù)name來獲取屬性值。
在這一步的時候,如果name屬性不存在,就會報錯:
throw new bindingexception("parameter '" + key + "' not found. available parameters are " + keyset());
name屬性是什么呢,有什么可選值呢?這就是處理接口形式的入?yún)⒑吞幚砑咸幚砗笏鶕碛械膋ey。
如果你遇到過類似異常,相信看到這兒就明白原因了。
數(shù)組、集合和map以外的object類型:
這種類型經(jīng)過了下面的處理:
1
2
|
metaobject metaobject = configuration.newmetaobject(parameterobject); bindings = new contextmap(metaobject); |
metaobject是mybatis的一個反射類,可以很方便的通過getvalue方法獲取對象的各種屬性(支持集合數(shù)組和map,可以多級屬性點.訪問,如user.username,user.roles[1].rolename)。 現(xiàn)在分析這種情況。
首先通過name獲取屬性時object result = map.get(name);,根據(jù)上面contextmap類中的get方法:
1
2
3
4
5
6
7
8
9
10
|
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 ; } |
可以看到這里會優(yōu)先從map中取該屬性的值,如果不存在,那么一定會執(zhí)行到下面這行代碼:
1
|
return parametermetaobject.getvalue(strkey) |
如果name剛好是對象的一個屬性值,那么通過metaobject反射可以獲取該屬性值。如果該對象不包含name屬性的值,就會報錯:
throw new reflectionexception("could not get property '" + prop.getname() + "' from " + object.getclass() + ". cause: " + t.tostring(), t);
理解這三種情況后,使用動態(tài)sql應(yīng)該不會有參數(shù)名方面的問題了。
在sql語句中使用參數(shù)
sql中的兩種形式#{username}或者${username},雖然看著差不多,但是實際處理過程差別很大,而且很容易出現(xiàn)莫名其妙的錯誤。
${username}的使用方式為ognl方式獲取值,和上面的動態(tài)sql一樣,這里先說這種情況。
${propertyname}參數(shù)
在textsqlnode.java中有一個內(nèi)部的靜態(tài)類bindingtokenparser,現(xiàn)在只看其中的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會創(chuàng)建一個默認為"value"的值,也就是說,在xml中的sql中可以直接使用${value},從else if可以看出來,只有是簡單類型的時候,才會有值。
關(guān)于這點,舉個簡單例子,如果接口為list<user> selectorderby(string column),如果xml內(nèi)容為:
1
2
3
|
<select id= "selectorderby" resulttype= "user" > select * from user order by ${value} </select> |
這種情況下,雖然沒有指定一個value屬性,但是mybatis會自動把參數(shù)column賦值進去。
再往下的代碼:
1
2
|
object value = ognlcache.getvalue(content, context.getbindings()); string srtvalue = (value == null ? "" : string.valueof(value)); |
這里和動態(tài)sql就一樣了,通過ognl方式來獲取值。
看到這里使用ognl這種方式時,你有沒有別的想法?
特殊用法:你是否在sql查詢中使用過某些固定的碼值?一旦碼值改變的時候需要改動很多地方,但是你又不想把碼值作為參數(shù)傳進來,怎么解決呢?你可能已經(jīng)明白了。
就是通過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支持的各種方法,如調(diào)用靜態(tài)方法。
#{propertyname}參數(shù)
這種方式比較簡單,復(fù)雜屬性的時候使用的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); } } } } |
上面這段代碼就是從參數(shù)中取#{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,當(dāng)使用<foreach>的時候,mybatis會自動生成額外的動態(tài)參數(shù),如果propertyname是動態(tài)參數(shù),就會從動態(tài)參數(shù)中取值。
- 第二個if,如果參數(shù)是null,不管屬性名是什么,都會返回null。
- 第三個if,如果參數(shù)是一個簡單類型,或者是一個注冊了typehandler的對象類型,就會直接使用該參數(shù)作為返回值,和屬性名無關(guān)。
- 最后一個else,這種情況下是復(fù)雜對象或者map類型,通過反射方便的取值。
下面我們說明上面四種情況下的參數(shù)名注意事項。
動態(tài)參數(shù),這里的參數(shù)名和值都由mybatis動態(tài)生成的,因此我們沒法直接接觸,也不需要管這兒的命名。但是我們可以了解一下這兒的命名規(guī)則,當(dāng)以后錯誤信息看到的時候,我們可以確定出錯的地方。
在foreachsqlnode.java中:
1
2
3
|
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循環(huán)產(chǎn)生的動態(tài)參數(shù)名就是:
__frch_user_0,__frch_user_1,__frch_user_2…
如果訪問動態(tài)參數(shù)的屬性,如user.username會被處理成__frch_user_0.username,這種參數(shù)值的處理過程在更早之前解析sql的時候就已經(jīng)獲取了對應(yīng)的參數(shù)值。具體內(nèi)容看下面有關(guān)<foreach>的詳細內(nèi)容。
參數(shù)為null,由于這里的判斷和參數(shù)名無關(guān),因此入?yún)ull的時候,在xml中寫的#{name}不管name寫什么,都不會出錯,值都是null。
可以直接使用typehandler處理的類型。最常見的就是基本類型,例如有這樣一個接口方法user selectbyid(@param("id")integer id),在xml中使用id的時候,我們可以隨便使用屬性名,不管用什么樣的屬性名,值都是id。
復(fù)雜對象或者map類型一般都是我們需要注意的地方,這種情況下,就必須保證入?yún)@些屬性,如果沒有就會報錯。這一點和可以參考上面有關(guān)metaobject的地方。
<foreach>詳解
所有動態(tài)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> |
對應(yīng)的接口:
1
|
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參數(shù)熟悉嗎?上面提到過很多。經(jīng)過一系列的參數(shù)處理后,這兒的bindings如下:
1
2
3
4
5
6
7
|
{ "_parameter" :{ "param1" :list, "userlist" :list }, "_databaseid" : null , } |
collectionexpression就是collection="userlist"的值userlist。
我們看看evaluator.evaluateiterable如何處理這個參數(shù),在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值的時候可能出現(xiàn)異常,具體可以參考上面動態(tài)sql部分的內(nèi)容。
userlist的值分四種情況。
- value == null,這種情況直接拋出異常builderexception。
- value instanceof iterable,實現(xiàn)iterable接口的直接返回,如collection的所有子類,通常是list。
- value.getclass().isarray()數(shù)組的情況,這種情況會轉(zhuǎn)換為list返回。
- value instanceof map如果是map,通過((map) value).entryset()返回一個set類型的參數(shù)。
通過上面處理后,返回的值,是一個iterable類型的值,這個值可以使用for (object o : iterable)這種形式循環(huán)。
在foreachsqlnode中對iterable循環(huán)的時候,有一段需要關(guān)注的代碼:
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,那么循環(huán)取得的子元素都是map.entry類型,這個時候會將mapentry.getkey()存儲到index中,mapentry.getvalue()存儲到item中。
如果是list,那么會將序號i存到index中,mapentry.getvalue()存儲到item中。
<foreach>常見錯誤補充
當(dāng)collection="userlist"的值userlist中的user是一個繼承自map的類型時,你需要保證<foreach>循環(huán)中用到的所有對象的屬性必須存在,map類型存在的問題通常是,如果某個值是null,一般是不存在相應(yīng)的key,這種情況會導(dǎo)致<foreach>出錯,會報找不到__frch_user_x參數(shù)。所以這種情況下,就是值是null,你也需要map.put(key,null)。
最后
這篇文章很長,寫這篇文章耗費的時間也很長,超過10小時,寫到半夜兩點都沒寫完。
這篇文章真的非常有用,如果你對mybatis有一定的了解,這篇文章幾乎是必讀的一篇。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對服務(wù)器之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
原文鏈接:https://blog.csdn.net/isea533/article/details/44002219