mybatis查詢語句的背後之引數解析
轉載請註明出處。。。
一、前言
通過前面我們也知道,通過getMapper方式來進行查詢,最後會通過mapperMehod類,對介面中傳來的引數也會在這個類裡面進行一個解析,隨後就傳到對應位置,與sql裡面的引數進行一個匹配,最後獲取結果。對於mybatis通常傳參(這裡忽略掉Rowbounds和ResultHandler兩種型別)有幾種方式。
1、javabean型別引數
2、非javabean型別引數
注意,本文是基於mybatis3.5.0版本進行分析。
1、引數的儲存
2、對sql語句中引數的賦值
下面將圍繞這這兩方面進行
二、引數的儲存
先看下面一段程式碼
1@Test 2public void testSelectOrdinaryParam() throws Exception{ 3SqlSession sqlSession = MybatisUtil.getSessionFactory().openSession(); 4UserMapper mapper = sqlSession.getMapper(UserMapper.class); 5List<User> userList = mapper.selectByOrdinaryParam("張三1號"); 6System.out.println(userList); 7sqlSession.close(); 8} 9List<User> selectByOrdinaryParam(String username);// mapper介面 10<select id="selectByOrdinaryParam" resultMap="BaseResultMap"> 11select 12<include refid="Base_Column_List"/> 13from user 14where username = #{username,jdbcType=VARCHAR} 15</select>
或許有的人會奇怪,這個mapper介面沒有帶@Param註解,怎麼能在mapper配置檔案中直接帶上引數名呢,不是會報錯嗎,
在mybatis裡面,對單個引數而言,直接使用引數名是沒問題的,如果是多個引數就不能這樣了,下面我們來了解下,mybatis的解析過程,請看下面程式碼,位於MapperMehod類的內部類MethodSignature建構函式中
1 public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { 2Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); 3if (resolvedReturnType instanceof Class<?>) { 4this.returnType = (Class<?>) resolvedReturnType; 5} else if (resolvedReturnType instanceof ParameterizedType) { 6this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); 7} else { 8this.returnType = method.getReturnType(); 9} 10this.returnsVoid = void.class.equals(this.returnType); 11this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); 12this.returnsCursor = Cursor.class.equals(this.returnType); 13this.returnsOptional = Optional.class.equals(this.returnType); 14this.mapKey = getMapKey(method); 15this.returnsMap = this.mapKey != null; 16this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); 17this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); 18// 引數解析類 19this.paramNameResolver = new ParamNameResolver(configuration, method); 20}
引數的儲存解析皆由ParamNameResolver類來進行操作,先看下該類的建構函式
1 /** 2* config 全域性的配置檔案中心 3* method 實際執行的方法,也就是mapper介面中的抽象方法 4* 5*/ 6 public ParamNameResolver(Configuration config, Method method) { 7// 獲取method中的所有引數型別 8final Class<?>[] paramTypes = method.getParameterTypes(); 9// 獲取引數中含有的註解,主要是為了@Param註解做準備 10final Annotation[][] paramAnnotations = method.getParameterAnnotations(); 11final SortedMap<Integer, String> map = new TreeMap<>(); 12// 這裡實際上獲取的值就是引數的個數。也就是二維陣列的行長度 13int paramCount = paramAnnotations.length; 14// get names from @Param annotations 15for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { 16// 排除RowBounds和ResultHandler兩種型別的引數 17if (isSpecialParameter(paramTypes[paramIndex])) { 18// skip special parameters 19continue; 20} 21String name = null; 22// 如果引數中含有@Param註解,則只用@Param註解的值作為引數名 23for (Annotation annotation : paramAnnotations[paramIndex]) { 24if (annotation instanceof Param) { 25hasParamAnnotation = true; 26name = ((Param) annotation).value(); 27break; 28} 29} 30// 即引數沒有@Param註解 31if (name == null) { 32// 引數實際名稱,其實這個值預設就是true,具體可以檢視Configuration類中的該屬性值,當然也可以在配置檔案進行配置關閉 33// 如果jdk處於1.8版本,且編譯時帶上了-parameters 引數,那麼獲取的就是實際的引數名,如methodA(String username) 34// 獲取的就是username,否則獲取的就是args0後面的數字就是引數所在位置 35if (config.isUseActualParamName()) { 36name = getActualParamName(method, paramIndex); 37} 38// 如果以上條件都不滿足,則將引數名配置為 0,1,2../ 39if (name == null) { 40// use the parameter index as the name ("0", "1", ...) 41// gcode issue #71 42name = String.valueOf(map.size()); 43} 44} 45map.put(paramIndex, name); 46} 47names = Collections.unmodifiableSortedMap(map); 48}
這個建構函式的作用就是對引數名稱進行一個封裝,得到一個 “引數位置-->引數名稱 “ 的一個map結構,這樣做的目的是為了替換引數值,我們也清楚,實際傳過來的引數就是一個一個Object陣列結構,我們也可以將它理解為map結構。即 index --> 引數值,此就和之前的 map結構有了對應,也就最終可以得到一個 引數名稱 ---> 引數值 的一個對應關係。
1 public Object execute(SqlSession sqlSession, Object[] args) { 2Object result; 3switch (command.getType()) { 4// 其它情況忽略掉 5case SELECT: 6// 這裡引數中含有resultHandler,暫不做討論 7if (method.returnsVoid() && method.hasResultHandler()) { 8executeWithResultHandler(sqlSession, args); 9result = null; 10} else if (method.returnsMany()) {// 1、 返回結果為集合型別或陣列型別,這種情況適用於大多數情況 11result = executeForMany(sqlSession, args); 12} else if (method.returnsMap()) {// 返回結果為Map型別 13result = executeForMap(sqlSession, args); 14} else if (method.returnsCursor()) { 15result = executeForCursor(sqlSession, args); 16} else {// 2、返回結果javabean型別,或普通的基礎型別及其包裝類等 17Object param = method.convertArgsToSqlCommandParam(args); 18result = sqlSession.selectOne(command.getName(), param); 19// 對java8中的optional進行了支援 20if (method.returnsOptional() && 21(result == null || !method.getReturnType().equals(result.getClass()))) { 22result = Optional.ofNullable(result); 23} 24} 25break; 26default: 27throw new BindingException("Unknown execution method for: " + command.getName()); 28} 29if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { 30throw new BindingException("Mapper method '" + command.getName() 31+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 32} 33return result; 34}
這裡主要分析1情況。對於2情況也就是接下來要說的引數賦值情況,不過要先介紹下method.convertArgsToSqlCommandParam這程式碼帶來的一個結果是怎麼樣的
1 public Object convertArgsToSqlCommandParam(Object[] args) { 2return paramNameResolver.getNamedParams(args); 3} 4 5 public Object getNamedParams(Object[] args) { 6final int paramCount = names.size(); 7if (args == null || paramCount == 0) { 8return null; 9} else if (!hasParamAnnotation && paramCount == 1) {// 1 10return args[names.firstKey()]; 11} else { 12final Map<String, Object> param = new ParamMap<>(); 13int i = 0; 14for (Map.Entry<Integer, String> entry : names.entrySet()) { 15param.put(entry.getValue(), args[entry.getKey()]); 16// add generic param names (param1, param2, ...) 17final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); 18// ensure not to overwrite parameter named with @Param 19if (!names.containsValue(genericParamName)) { 20param.put(genericParamName, args[entry.getKey()]); 21} 22i++; 23} 24return param; 25} 26}
可以很清楚的知道最後又呼叫了ParamNameResolver類的getNamedPaams方法,這個方法的主要作用就是,將原來的引數位置 --> 引數名稱 對映關係轉為 引數名稱 --->引數值 ,並且新加一個引數名和引數值得一個對應關係。即
param1 ->引數值1
param2 -->引數值2
當然如果只有一個引數,如程式碼中的1部分,若引數沒有@Param註解,且只有一個引數,則不會加入上述的一個物件關係,這也就是前面說的,對於單個引數,可以直接在sql中寫引數名就ok的原因。下面回到前面
1 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 2List<E> result; 3// 獲取對應的一個對映關係,param型別有可能為map或null或引數實際型別 4Object param = method.convertArgsToSqlCommandParam(args); 5if (method.hasRowBounds()) { 6RowBounds rowBounds = method.extractRowBounds(args); 7result = sqlSession.<E>selectList(command.getName(), param, rowBounds); 8} else { 9result = sqlSession.<E>selectList(command.getName(), param); 10} 11// 如果返回結果型別和method的返回結果型別不一致,則進行轉換資料結構 12// 其實就是result返回結果不是List型別,而是其他集合型別或陣列型別 13if (!method.getReturnType().isAssignableFrom(result.getClass())) { 14if (method.getReturnType().isArray()) {// 為陣列結果 15return convertToArray(result); 16} else {// 其他集合型別 17return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 18} 19} 20return result; 21}
程式碼也不復雜,就是將得到的引數對應關係傳入,最終獲取結果,根據實際需求進行結果轉換。
3、對sql語句中引數的賦值
其實前面一篇部落格中也有涉及到。引數賦值的位置在DefaultParameterHandler類裡面,可以檢視前面一篇部落格,這裡不做過多介紹,傳送門 mybatis查詢語句的背後之封裝資料
---------------------------------------------------------------------------------------------------------------------------------------分界線--------------------------------------------------------------------------------------------------------
若有不足或錯誤之處,還望指正,謝謝!