1. 问题描述

  在使用MyBatis,我们经常会遇到这种情况:SELECT两个字段,需要返回一个Map,其中第一个字段作为key,第二个字段作为value。MyBatis的MapKey虽然很实用,但并不能解决这种场景。这里,就介绍一种使用拦截器来解决这个问题的方案。

2. 解决方案

源码详见:spring-mybatis-test

2.1 注解

  1. package com.adu.spring_test.mybatis.annotations;
  2.  
  3. import java.lang.annotation.Documented;
  4. import java.lang.annotation.ElementType;
  5. import java.lang.annotation.Retention;
  6. import java.lang.annotation.RetentionPolicy;
  7. import java.lang.annotation.Target;
  8.  
  9. /**
  10. * 将查询结果映射成map的注解,其中第一个字段为key,第二个字段为value.
  11. * <p>
  12. * 注:返回类型必须为{@link java.util.Map Map<K, V>}。K/V的类型通过MyBatis的TypeHander进行类型转换,如有必要可自定义TypeHander。
  13. *
  14. * @author yunjie.du
  15. * @date 2016/12/22 18:44
  16. */
  17. @Documented
  18. @Retention(RetentionPolicy.RUNTIME)
  19. @Target({ ElementType.METHOD })
  20. public @interface MapF2F {
  21. /**
  22. * 是否允许key重复。如果不允许,而实际结果出现了重复,会抛出org.springframework.dao.DuplicateKeyException。
  23. *
  24. * @return
  25. */
  26. boolean isAllowKeyRepeat() default true;
  27.  
  28. /**
  29. * 对于相同的key,是否允许value不同(在允许key重复的前提下)。如果允许,则按查询结果,后面的覆盖前面的;如果不允许,则会抛出org.springframework.dao.DuplicateKeyException。
  30. *
  31. * @return
  32. */
  33. boolean isAllowValueDifferentWithSameKey() default false;
  34. }

2.2 拦截器

  1. package com.adu.spring_test.mybatis.interceptor;
  2.  
  3. import java.lang.reflect.Method;
  4. import java.lang.reflect.ParameterizedType;
  5. import java.lang.reflect.Type;
  6. import java.sql.ResultSet;
  7. import java.sql.SQLException;
  8. import java.sql.Statement;
  9. import java.util.ArrayList;
  10. import java.util.HashMap;
  11. import java.util.List;
  12. import java.util.Map;
  13. import java.util.Objects;
  14. import java.util.Properties;
  15.  
  16. import org.apache.commons.lang3.StringUtils;
  17. import org.apache.ibatis.executor.resultset.ResultSetHandler;
  18. import org.apache.ibatis.mapping.MappedStatement;
  19. import org.apache.ibatis.plugin.Interceptor;
  20. import org.apache.ibatis.plugin.Intercepts;
  21. import org.apache.ibatis.plugin.Invocation;
  22. import org.apache.ibatis.plugin.Plugin;
  23. import org.apache.ibatis.plugin.Signature;
  24. import org.apache.ibatis.reflection.MetaObject;
  25. import org.apache.ibatis.type.TypeHandler;
  26. import org.apache.ibatis.type.TypeHandlerRegistry;
  27. import org.slf4j.Logger;
  28. import org.slf4j.LoggerFactory;
  29. import org.springframework.dao.DuplicateKeyException;
  30.  
  31. import com.adu.spring_test.mybatis.annotations.MapF2F;
  32. import com.adu.spring_test.mybatis.util.ReflectUtil;
  33.  
  34. import javafx.util.Pair;
  35.  
  36. /**
  37. * MapF2F的拦截器
  38. *
  39. * @author yunjie.du
  40. * @date 2016/12/22 18:44
  41. */
  42. @Intercepts(@Signature(method = "handleResultSets", type = ResultSetHandler.class, args = { Statement.class }))
  43. public class MapF2FInterceptor implements Interceptor {
  44. private Logger logger = LoggerFactory.getLogger(MapF2FInterceptor.class);
  45.  
  46. @Override
  47. public Object intercept(Invocation invocation) throws Throwable {
  48. MetaObject metaStatementHandler = ReflectUtil.getRealTarget(invocation);
  49. MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("mappedStatement");
  50.  
  51. String className = StringUtils.substringBeforeLast(mappedStatement.getId(), ".");// 当前类
  52. String currentMethodName = StringUtils.substringAfterLast(mappedStatement.getId(), ".");// 当前方法
  53. Method currentMethod = findMethod(className, currentMethodName);// 获取当前Method
  54.  
  55. if (currentMethod == null || currentMethod.getAnnotation(MapF2F.class) == null) {// 如果当前Method没有注解MapF2F
  56. return invocation.proceed();
  57. }
  58.  
  59. // 如果有MapF2F注解,则这里对结果进行拦截并转换
  60. MapF2F mapF2FAnnotation = currentMethod.getAnnotation(MapF2F.class);
  61. Statement statement = (Statement) invocation.getArgs()[0];
  62. Pair<Class<?>, Class<?>> kvTypePair = getKVTypeOfReturnMap(currentMethod);// 获取返回Map里key-value的类型
  63. TypeHandlerRegistry typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();// 获取各种TypeHander的注册器
  64. return result2Map(statement, typeHandlerRegistry, kvTypePair, mapF2FAnnotation);
  65.  
  66. }
  67.  
  68. @Override
  69. public Object plugin(Object obj) {
  70. return Plugin.wrap(obj, this);
  71. }
  72.  
  73. @Override
  74. public void setProperties(Properties properties) {
  75.  
  76. }
  77.  
  78. /**
  79. * 找到与指定函数名匹配的Method。
  80. *
  81. * @param className
  82. * @param targetMethodName
  83. * @return
  84. * @throws Throwable
  85. */
  86. private Method findMethod(String className, String targetMethodName) throws Throwable {
  87. Method[] methods = Class.forName(className).getDeclaredMethods();// 该类所有声明的方法
  88. if (methods == null) {
  89. return null;
  90. }
  91.  
  92. for (Method method : methods) {
  93. if (StringUtils.equals(method.getName(), targetMethodName)) {
  94. return method;
  95. }
  96. }
  97.  
  98. return null;
  99. }
  100.  
  101. /**
  102. * 获取函数返回Map中key-value的类型
  103. *
  104. * @param mapF2FMethod
  105. * @return left为key的类型,right为value的类型
  106. */
  107. private Pair<Class<?>, Class<?>> getKVTypeOfReturnMap(Method mapF2FMethod) {
  108. Type returnType = mapF2FMethod.getGenericReturnType();
  109.  
  110. if (returnType instanceof ParameterizedType) {
  111. ParameterizedType parameterizedType = (ParameterizedType) returnType;
  112. if (!Map.class.equals(parameterizedType.getRawType())) {
  113. throw new RuntimeException(
  114. "[ERROR-MapF2F-return-map-type]使用MapF2F,返回类型必须是java.util.Map类型!!!method=" + mapF2FMethod);
  115. }
  116.  
  117. return new Pair<>((Class<?>) parameterizedType.getActualTypeArguments()[0],
  118. (Class<?>) parameterizedType.getActualTypeArguments()[1]);
  119. }
  120.  
  121. return new Pair<>(null, null);
  122. }
  123.  
  124. /**
  125. * 将查询结果映射成Map,其中第一个字段作为key,第二个字段作为value.
  126. *
  127. * @param statement
  128. * @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
  129. * @param kvTypePair 函数指定返回Map key-value的类型
  130. * @param mapF2FAnnotation
  131. * @return
  132. * @throws Throwable
  133. */
  134. private Object result2Map(Statement statement, TypeHandlerRegistry typeHandlerRegistry,
  135. Pair<Class<?>, Class<?>> kvTypePair, MapF2F mapF2FAnnotation) throws Throwable {
  136. ResultSet resultSet = statement.getResultSet();
  137. List<Object> res = new ArrayList();
  138. Map<Object, Object> map = new HashMap();
  139.  
  140. while (resultSet.next()) {
  141. Object key = this.getObject(resultSet, 1, typeHandlerRegistry, kvTypePair.getKey());
  142. Object value = this.getObject(resultSet, 2, typeHandlerRegistry, kvTypePair.getValue());
  143.  
  144. if (map.containsKey(key)) {// 该key已存在
  145. if (!mapF2FAnnotation.isAllowKeyRepeat()) {// 判断是否允许key重复
  146. throw new DuplicateKeyException("MapF2F duplicated key!key=" + key);
  147. }
  148.  
  149. Object preValue = map.get(key);
  150. if (!mapF2FAnnotation.isAllowValueDifferentWithSameKey() && !Objects.equals(value, preValue)) {// 判断是否允许value不同
  151. throw new DuplicateKeyException("MapF2F different value with same key!key=" + key + ",value1="
  152. + preValue + ",value2=" + value);
  153. }
  154. }
  155.  
  156. map.put(key, value);// 第一列作为key,第二列作为value。
  157. }
  158.  
  159. res.add(map);
  160. return res;
  161. }
  162.  
  163. /**
  164. * 结果类型转换。
  165. * <p>
  166. * 这里借用注册在MyBatis的typeHander(包括自定义的),方便进行类型转换。
  167. *
  168. * @param resultSet
  169. * @param columnIndex 字段下标,从1开始
  170. * @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
  171. * @param javaType 要转换的Java类型
  172. * @return
  173. * @throws SQLException
  174. */
  175. private Object getObject(ResultSet resultSet, int columnIndex, TypeHandlerRegistry typeHandlerRegistry,
  176. Class<?> javaType) throws SQLException {
  177. final TypeHandler<?> typeHandler = typeHandlerRegistry.hasTypeHandler(javaType)
  178. ? typeHandlerRegistry.getTypeHandler(javaType) : typeHandlerRegistry.getUnknownTypeHandler();
  179.  
  180. return typeHandler.getResult(resultSet, columnIndex);
  181.  
  182. }
  183.  
  184. }

2.3 ReflectUtil

  1. package com.adu.spring_test.mybatis.util;
  2.  
  3. import org.apache.ibatis.plugin.Invocation;
  4. import org.apache.ibatis.reflection.MetaObject;
  5. import org.apache.ibatis.reflection.SystemMetaObject;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8.  
  9. /**
  10. * 反射工具类
  11. */
  12. public class ReflectUtil {
  13. private static final Logger logger = LoggerFactory.getLogger(ReflectUtil.class);
  14.  
  15. /**
  16. * 分离最后一个代理的目标对象
  17. *
  18. * @param invocation
  19. * @return
  20. */
  21. public static MetaObject getRealTarget(Invocation invocation) {
  22. MetaObject metaStatementHandler = SystemMetaObject.forObject(invocation.getTarget());
  23.  
  24. while (metaStatementHandler.hasGetter("h")) {
  25. Object object = metaStatementHandler.getValue("h");
  26. metaStatementHandler = SystemMetaObject.forObject(object);
  27. }
  28.  
  29. while (metaStatementHandler.hasGetter("target")) {
  30. Object object = metaStatementHandler.getValue("target");
  31. metaStatementHandler = SystemMetaObject.forObject(object);
  32. }
  33.  
  34. return metaStatementHandler;
  35. }
  36.  
  37. }

2.4 MyBatis Datasource配置拦截器

  1. <!-- session factory -->
  2. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  3. <property name="dataSource" ref="dataSource" />
  4. <property name="configLocation" value="classpath:mybatis/mybatis-data-config.xml" />
  5. <property name="mapperLocations" value="classpath:mapper/**/*.xml" />
  6. <property name="plugins">
  7. <array>
  8. <bean class="com.adu.spring_test.mybatis.interceptor.MapF2FInterceptor"/>
  9. </array>
  10. </property>
  11. </bean>

2.5 简例

  1. /**
  2. * 批量获取用户姓名
  3. *
  4. * @param ids
  5. * @return key为ID,value为username
  6. */
  7. @MapF2F()
  8. Map<Long, String> queryUserNamesByIds(@Param("ids") List<Long> ids);

  

  1. <select id="queryUserNamesByIds" resultType="map">
  2. SELECT id, user_name
  3. FROM user_info
  4. WHERE id IN
  5. <foreach collection="ids" open="(" close=")" separator="," item="item">
  6. #{item}
  7. </foreach>
  8. </select>

参考:

 

MyBatis查询两个字段,返回Map,一个字段作为key,一个字段作为value的实现的更多相关文章

  1. node连续查询两次数据库返回方式(文档未定)

    function db(callback){ var mysql = require('mysql'); var connection = mysql.createConnection({ host ...

  2. MyBatis 返回 Map 字段丢失问题

    问题现象 执行存储过程返回 Map 集合数据,发现有字段丢失情况,仔细研究发现丢失的字段值都为 NULL. 解决办法1: 在查询 SQL 语句中增加 NULL 判断函数 MSSQL: isnull(字 ...

  3. spring boot整合mybatis查询数据库返回Map字段为空不返回解决

    1.出现问题原因原因1:mybatis的配置即mapper返回映射配置. 原因2:jackson的配置即@ResponseBody序列化配置. 2.解决方式步骤1:解决原因1 mybatis: con ...

  4. mybatis返回map类型数据空值字段不显示(三种解决方法)

    转http://blog.csdn.net/lulidaitian/article/details/70941769 一.查询sql添加每个字段的判断空 IFNULL(rate,'') as rate ...

  5. mybatis返回map类型数据空值字段不显示的解决方法

    在日常开发中,查询数据返回类型为map,数据库中有些自动值为null,则返回的结果中没有值为空的字段,则如何显示值为空的字段呢? Spring boot + MyBatis返回map中null值默认不 ...

  6. 【mybatis】mybatis查询 结果 用map接收,无实体接收 + 关联子表 一并返回主子表的结果

    如果后台程序没有实体对应mysql的数据表. 而mybatis想要查询mysql这个数据表的数据,返回给应用程序. 应用程序该如何接收? =============================== ...

  7. 使用MyBatis时接收值和返回值选择Map类型或者实体类型

    MyBatis作为现近JavaEE企业级项目开发中常用的持久层框架之一,以其简洁高效的ORM映射和高度的SQL的自由性被广大开发人员认可.Mybatis在接收系统传来的参数和返回的参数时主要可以有Ma ...

  8. Mybatis,返回Map的时候,将Map内的Key转换为驼峰的命名

    每次使用mybatis的时候,简单的连表查询,用Map接收的时候,都是像DB定义的字段一样,类似以下 student_name,student_id,没有转换为驼峰,但是又不能因为这一个定义一个jav ...

  9. 【问题记录】MyBatis查询数据库返回多个不同类型参数的结果集的接收方式

    其实是个非常简单的问题,但是这玩意儿弄得我很难受,又浪费了一个下午的时间,简直了…… 问题大概是,我在查询数据库时,查询的结果有两个,一个是varchar格式的字段,一个int格式字段,例如: sel ...

随机推荐

  1. 站内信DB设计实现

    两年前,万仓一黍在博客园发了两篇关于站内信的设计实现博文,<群发"站内信"的实现>.<群发"站内信"的实现(续)>,其中阐述了他关于站内 ...

  2. [网络] SOCKET, TCP/UDP, HTTP, FTP

    (一)TCP/UDP,SOCKET,HTTP,FTP简析 TCP/IP是个协议组,可分为三个层次:网络层.传输层和应用层: 网络层:IP协议.ICMP协议.ARP协议.RARP协议和BOOTP协议 传 ...

  3. IOS开发之Post 方式获取服务器数据

    //1.创建post方式的 参数字符串url +(NSString *)createPostURL:(NSMutableDictionary *)params { NSString *postStri ...

  4. 定时关机命令——shutdown

    通常会用到的定时关机命令有两种: Shutdown -s -t 36001小时后自己主动关机(3600秒) at 12:00 Shutdown -s 12:00自己主动关闭计算机 系统定时关机: Wi ...

  5. Android学习四、Android中的Adapter

    一.Adapter的介绍 An Adapter object acts as a bridge between an AdapterView and the underlying data for t ...

  6. JS和利用openssl的object C加密得到相同的aes加密密文

    这是之前接到的一个工作内容,项目原本的登录操作是获得账号和密码以后,对密码进行一遍MD5加密,然后传递账号和密文到cgi文件.在c中获取到账户以后,从数据库中获取到密码,对密码进行一次MD5的加密,然 ...

  7. java 对list中对象按属性排序

    实体对象类 --略 排序类----实现Comparator接口,重写compare方法 package com.tang.list; import java.util.Comparator; publ ...

  8. 几种破解MySQL root密码的几种方法:

    几种破解MySQL root密码的几种方法: 方法一 使用phpmyadmin,这是最简单的了,修改mysql库的user表,不过别忘了使用PASSWord函数. 方法二 使用mysqladmin,这 ...

  9. 武汉科技大学ACM:1010: 电话号码

    Problem Description LXD打算换个手机号码,但是他去营业厅选号码的时候却把移动的客服小姐烦得不行,因为他太挑三捡四啦.对于一个手机号的后六位数字(前面五位他就无所谓了),LXD有很 ...

  10. Eclipse导入JavaWeb项目报错:The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path

    JavaWeb项目中写的JSP页面需要Web容器解析处理成HTML才能展示到前端浏览器,解析JSP需要Web容器.JSP页面顶端出现“红色”的报错信息:The superclass "jav ...