一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 深入了解MyBatis參數

深入了解MyBatis參數

2021-06-24 10:54isea533 Java教程

今天小編就為大家分享一篇關于深入了解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會首先經過下面方法來轉換參數:

?
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);

那么他對應的params如下:

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

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

  • 入參為null或沒有時,參數轉換為null
  • 沒有使用@param注解并且只有一個參數時,返回這一個參數
  • 使用了@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>中默認情況下寫的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中,從構造方法看起:

?
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就是我們經過前面兩步處理后的參數。這個參數經過前面兩步處理后,到這里的時候,他只有下面三種情況:

  • null,如果沒有入參或者入參是null,到這里也是null。
  • map類型,除了null之外,前面兩步主要是封裝成map類型。
  • 數組、集合和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
@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類型:

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

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

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

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

?
1
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內容為:

?
1
2
3
<select id="selectorderby" resulttype="user">
select * from user order by ${value}
</select>

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

再往下的代碼:

?
1
2
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類型,通過反射方便的取值。

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

動態參數,這里的參數名和值都由mybatis動態生成的,因此我們沒法直接接觸,也不需要管這兒的命名。但是我們可以了解一下這兒的命名規則,當以后錯誤信息看到的時候,我們可以確定出錯的地方。
在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循環產生的動態參數名就是:

__frch_user_0,__frch_user_1,__frch_user_2…

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

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

可以直接使用typehandler處理的類型。最常見的就是基本類型,例如有這樣一個接口方法user selectbyid(@param("id")integer id),在xml中使用id的時候,我們可以隨便使用屬性名,不管用什么樣的屬性名,值都是id。

復雜對象或者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>

對應的接口:

?
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參數熟悉嗎?上面提到過很多。經過一系列的參數處理后,這兒的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的值分四種情況。

  • 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循環的時候,有一段需要關注的代碼:

?
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中。

<foreach>常見錯誤補充

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

最后

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

這篇文章真的非常有用,如果你對mybatis有一定的了解,這篇文章幾乎是必讀的一篇。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對服務器之家的支持。如果你想了解更多相關內容請查看下面相關鏈接

原文鏈接:https://blog.csdn.net/isea533/article/details/44002219

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 青青91 | crdy在线看亚洲 | 亚洲国产精品第一页 | w7w7w7w7w免费 | 天天狠天天天天透在线 | 973影院| 四虎小视频 | 欧美成人aaaa免费高清 | 欧美日韩亚洲一区二区三区在线观看 | 天码毛片一区二区三区入口 | 欧美日韩中文国产一区 | 国产激情视频在线 | av在线色 | 久久免费特黄毛片 | 成人男女啪啪免费观看网站 | 亚洲图片一区二区 | 久久一er精这里有精品 | 福利片免费一区二区三区 | 亚洲电影成人 成人影院 | 97自拍视频在线观看 | 成人啪啪漫画羞羞漫画www网站 | 久久亚洲精品中文字幕60分钟 | 国产精品99久久免费观看 | 男人的天堂久久爱 | 91久久夜色精品国产九色 | 互换身体全集免费观看 | 99国产精品免费观看视频 | 日本一道本中文字幕 | 韩国三级年轻的小婊孑 | 91午夜剧场| 国产午夜免费秋霞影院 | 50度灰破解版v5.7.0 | 日本视频在线观看 | 欧美二区视频 | 好大好爽好涨太深了小喜 | 欧美一区二区三区大片 | 黄a级| 午夜精品免费 | 情乱奶水欲 | 国产里番 | 爱豆传媒最新视频国产 |