MyBatis处理多参数及原理分析
一、多參數處理方式
1.1使用@Param注解
MyBatis 允許在mapper 接口中使用@Param注解來處理多個參數。
mapper 接口:
/** @Param 注解中的值可以是任意的 */Employee getEmpByIdAndUsername(@Param("id") Integer id, @Param("username") String username);SQL 映射文件:
<!-- 這里#{} 中的id 與username,對應上面@Param 注解中的值 --><select id="getEmpByIdAndUsername" resultType="employee">select * from t_employee where id = #{id} and username = #{username}</select>1.2使用Map封裝多個參數
MyBatis 允許使用Map來封裝需要在SQL 中傳入的參數。
mapper 接口:
Employee getEmpByMap(Map<String, Object> map);SQL 映射文件:
<!-- #{} 中的值要與在map 中定義的key 一致 --><select id="getEmpByMap" resultType="employee">select * from t_employee where id = #{id} and username = #{username}</select>測試代碼,使用Map封裝傳遞的參數:
// 用來封裝在SQL 語句中需要傳遞的參數Map<String, Object> map = new HashMap<>();map.put("id", 1);map.put("username", "張三");Employee employee = employeeMapper.getEmpByMap(map);1.3使用#{param1 ... n}參數
如果我們既不用@Param也不使用map,那么可以在編寫SQL 語句的時候,使用#{param1 ... n}來代表需要傳遞的參數。
mapper 接口:
Employee getEmpByIdAndGender(Integer id, String username);SQL 映射文件:
<!-- 通過使用#{param1 ... n}的方式來傳遞參數 --><select id="getEmpByIdAndGender" resultType="employee">select * from t_employee where id = #{param1} and gender = #{param2}</select>1.4發散思維
比如遇到下面的幾種情況時,我們要能夠隨機應變:
/** 第一個參數加@Param 注解,后面一個參數不加 */Employee getEmpByIdAndEmail(@Param("id") Integer id, String email);/** 編寫SQL 參數對應方式 */id ==> #{param1}或id, email ==> #{param2} /** 第二個參數是一個JavaBean 類型 */Employee getEmpByIdAndEmail(Integer id,@Param("employee") Employee email);/** 編寫SQL 參數對應方式 */id ==> #{param1}, email ==> #{param2.email} 或employee.email1.5建議與小結
建議:在書寫多參數的SQL 語句時,如果需要傳遞的參數都是某個JavaBean 的屬性,那么建議傳入單個的JavaBean,通過JavaBean 來封裝需要傳遞的參數。
比如改寫Employee getEmpByIdAndGender(Integer id, String username);方法。
mapper 接口:
Employee getEmpByIdAndGender(Employee employee);SQL 映射文件:
<!-- 在select 標簽中指定parameterType(傳進來的參數類型)是某個JavaBean 類型,也可以省略#{} 中的參數要與對應的JavaBean 的屬性一致--><select id="getEmpByIdAndGender" parameterType="employee" resultType="employee">select * from t_employee where id = #{id} and gender = #{gender}</select>小結:在傳入單個參數中,在編寫SQL 語句時#{ }中的值可以是任意的,因為MyBatis 會自動對這個參數進行處理。在傳遞多個參數時,其實MyBatis 也會自動對多參數進行處理,會將這些參數在底層封裝成一個Map。下面就通過DEBUG 的方式來一探究竟。
二、多參數封裝成Map原理解析
這里我們以#{param1 ... n}傳遞參數為例,也就是上面1.3 所使用的方式,下面是其測試代碼(省略了getSqlSession()方法):
@Testpublic void parameterTest() throws IOException {SqlSession sqlSession = getSqlSession();EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);Employee employee = employeeMapper.getEmpByIdAndGender(15, "1");System.out.println(employee);}1.將斷點打在Employee employee = employeeMapper.getEmpByIdAndGender(15, "1");上,會跳入到MapperProxy<T>類中的invoke(Object proxy, Method method, Object[] args)方法。
/** MapperProxy<T>類中的方法 */@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (isDefaultMethod(method)) {return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}在執行到最后一行return mapperMethod.execute(sqlSession, args);時,下面是其對應的參數信息:
2.接著打斷點執行會跳到MapperMethod類中的execute(SqlSession sqlSession, Object[] args)方法,在這個方法中會對SQL 語句的類型進行判斷,因為是SELECT語句,為了防止多余的代碼太多,故刪除了一些用不到的信息。
3.接著跟蹤,會跳到MapperMethod類中的convertArgsToSqlCommandParam(Object[] args) 方法,其中args中任然保存著傳入的參數信息。
/** MapperMethod 類中的方法 */public Object convertArgsToSqlCommandParam(Object[] args) {return paramNameResolver.getNamedParams(args);}4.接著打斷點,會跳到ParamNameResolver類中的getNamedParams(Object[] args)方法,根據類名我們應該知道了,應該就是在這個類中對參數進行轉換的。
/** ParamNameResolver 類中的方法 */public Object getNamedParams(Object[] args) {final int paramCount = names.size();if (args == null || paramCount == 0) {return null;} else if (!hasParamAnnotation && paramCount == 1) {return args[names.firstKey()];} else {final Map<String, Object> param = new ParamMap<Object>();int i = 0;for (Map.Entry<Integer, String> entry : names.entrySet()) {param.put(entry.getValue(), args[entry.getKey()]);// add generic param names (param1, param2, ...)final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);// ensure not to overwrite parameter named with @Paramif (!names.containsValue(genericParamName)) {param.put(genericParamName, args[entry.getKey()]);}i++;}return param;}}在上面這個方法中有一個names屬性,這個names是什么呢?names 其實是一個全局變量,并且是一個Map,Map中的key用于保存索引,value就是要保存的參數名。names通過ParamNameResolver類的構造函數來實例化存儲信息。接著就進入到重點部分了。下面是ParamNameResolver類的構造函數:
/*** 下面是官方說明的names 存儲規則:* 如果使用注解,則names 的key 保存索引,value 保存注解中的值* 不使用下標,names 中key 保存索引,value 保存的也是參數的索引* 特殊的參數在這里我們就不考慮了* * <li>aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}</li>* <li>aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}</li>* <li>aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}</li>* </ul>*/private final SortedMap<Integer, String> names; /** ParamNameResolver 類的構造函數 */public ParamNameResolver(Configuration config, Method method) {final Class<?>[] paramTypes = method.getParameterTypes();final Annotation[][] paramAnnotations = method.getParameterAnnotations();final SortedMap<Integer, String> map = new TreeMap<Integer, String>();int paramCount = paramAnnotations.length;// get names from @Param annotationsfor (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {if (isSpecialParameter(paramTypes[paramIndex])) {// skip special parameterscontinue;}String name = null;for (Annotation annotation : paramAnnotations[paramIndex]) {/*** 對當前注解判斷,如果是Param 注解,就把Param 注解中的值賦給name,* 因為我們沒有使用注解,所以name 為null*/if (annotation instanceof Param) {hasParamAnnotation = true;name = ((Param) annotation).value();break;}}if (name == null) {// 如果沒有指定@Param 注解,在這里通過getActualParamName(method, paramIndex) 方法為name 賦值if (config.isUseActualParamName()) {// 這里就不細節講述是如何獲得name 的值的了,有興趣的可以自己查看源碼進行了解name = getActualParamName(method, paramIndex);}// 到這里name 已經有值了,會直接跳過if (name == null) {// use the parameter index as the name ("0", "1", ...)// gcode issue #71name = String.valueOf(map.size());}// 在map 中存儲信息,key 表示索引,value 為獲得的namemap.put(paramIndex, name);}// 為names 賦值names = Collections.unmodifiableSortedMap(map);}下面是執行完構造函數后,names中保存的信息:
其實在給names的value賦值時,還加了arg前綴。PS:這里我們在進行測試的時候并沒有使用@Param注解,如果使用的是@Param注解,那么names 的value保存的應該就是在@Param中設置的值。
分析完names中保存的是什么信息后,我們就可以對上面的getNamedParams(Object[] args)方法進行解析了。
/** ParamNameResolver 類中的方法 */public Object getNamedParams(Object[] args) {// 此時paramCount 為2final int paramCount = names.size();// 如果為空,直接返回if (args == null || paramCount == 0) {return null;} else if (!hasParamAnnotation && paramCount == 1) {// 如果沒有使用注解并且只有一個參數,直接返回return args[names.firstKey()];} else {final Map<String, Object> param = new ParamMap<Object>();int i = 0;for (Map.Entry<Integer, String> entry : names.entrySet()) {/*** 遍歷循環,把names 的value作為param 的key,names 的key 作為param 的value* 存儲為{"arg0",0},{"arg1",1} 的格式*/param.put(entry.getValue(), args[entry.getKey()]);// GENERIC_NAME_PREFIX 的值是"param",繼續進行處理,genericParamName 會是param1 ... n final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);/*** 把genericParamName 作為key,傳遞進來的SQL參數作為value* 存儲為{"param1", "傳遞進來的參數"} 的格式* 這樣做的好處是即使你使用了@Param注解,你也可以通過使用#{param1 ... n}的方式來進行參數傳遞*/if (!names.containsValue(genericParamName)) {param.put(genericParamName, args[entry.getKey()]);}i++;}// 最后返回paramreturn param;}}最終param中保存的數據為:
5.接著一步步返回,返回的時候還進行了很多步驟,最終會執行到
result = sqlSession.selectOne(command.getName(), param);,
也就是返回到MapperedMethod中的(SqlSession sqlSession, Object[] args)方法,到這里就可以把信息查詢出來了。
三、總結
這篇博客主要介紹了MyBatis 處理多參數的方法,以及需要注意的地方。學習不僅要知其然還要知其所以然,所以在博文的后半部分通過DEBUG 的方式對MyBatis 處理多參數封裝成Map的原理進行了分析。建議大家自己動手去分析源碼的底層,去了解更多的有趣的知識,希望這篇博文能夠為讀者提供幫助。
總結
以上是生活随笔為你收集整理的MyBatis处理多参数及原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 山东黄金是国企吗
- 下一篇: 上证380指数是什么