TooManyResultsException,Expect one result or null to be return by selectOne...

1、场景
select语句返回两列数据,需要以kv的格式接收,于是自定义实现了一个CustMap拦截器,只要使用了CustMap,就会对sql的返回结果拦截重新封装为Map<K, V>结构返回,测试环境能够正常使用,上线到生产环境后就一直报TooManyResultsException,Expect one result or null to be return by selectOne…
2、分析
Mybatis的动态代理(此处不介绍,有需要的可以自行搜索了解),底层默认是selectOne方法,返回为List需要选择selectList执行,怎么选择的自行研究源码。
3、解决
将Map<K, V>使用List封装List<Map<K, V>>或者直接使用实例List<实例>,这两种方式都可以避免此问题。
备注:未定位到测试环境和生产环境(各个组件版本一致)两种不同结果的原因,有思路的可以留言。

/**
 * 自定义Map注解
 * 将mybatis的select查询的两列值转为map的kv值
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CustomMap {
    /**
     * 是否允许key重复。如果不允许,而实际结果出现了重复,会抛出org.springframework.dao.DuplicateKeyException。
     */
    boolean isAllowKeyRepeat() default true;

    /**
     * 对于相同的key,是否允许value不同(在允许key重复的前提下)。如果允许,则按查询结果,后面的覆盖前面的;如果不允许,则会抛出org.springframework.dao.DuplicateKeyException。
     */
    boolean isAllowValueDifferentWithSameKey() default false;
}
@Intercepts(@Signature(method = "handleResultSets", type = ResultSetHandler.class, args = {Statement.class}))
public class CustomMapResultPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaStatementHandler = ReflectUtil.getRealTarget(invocation);
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("mappedStatement");
        // 当前类
        String className = StringUtils.substringBeforeLast(mappedStatement.getId(), ".");
        // 当前方法
        String currentMethodName = StringUtils.substringAfterLast(mappedStatement.getId(), ".");
        // 获取当前Method
        Method currentMethod = findMethod(className, currentMethodName);
        if (currentMethod == null || currentMethod.getAnnotation(CustomMap.class) == null) {
            // 如果当前Method没有注解CustomMap
            return invocation.proceed();
        }
        // 如果有CustomMap注解,则这里对结果进行拦截并转换
        CustomMap customMap = currentMethod.getAnnotation(CustomMap.class);
        Statement statement = (Statement) invocation.getArgs()[0];
        // 获取返回Map里key-value的类型
        Pair<Class<?>, Class<?>> kvTypePair = getKVTypeOfReturnMap(currentMethod);
        // 获取各种TypeHander的注册器
        TypeHandlerRegistry typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
        return result2Map(statement, typeHandlerRegistry, kvTypePair, customMap);
    }

    @Override
    public Object plugin(Object obj) {
        return Plugin.wrap(obj, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 找到与指定函数名匹配的Method。
     */
    private Method findMethod(String className, String targetMethodName) throws Throwable {
        // 该类所有声明的方法
        Method[] methods = Class.forName(className).getDeclaredMethods();
        for (Method method : methods) {
            if (StringUtils.equals(method.getName(), targetMethodName)) {
                return method;
            }
        }
        return null;
    }

    /**
     * 获取函数返回Map中key-value的类型
     *
     * @return left为key的类型,right为value的类型
     */
    private Pair<Class<?>, Class<?>> getKVTypeOfReturnMap(Method method) {
        Type returnType = method.getGenericReturnType();


        if (returnType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) returnType;
            if (!Map.class.equals(parameterizedType.getRawType())) {
                throw new RuntimeException(
                        "[ERROR-CustomMap-return-Map-type]使用CustomMap,返回类型必须是java.util.Map类型!!!method=" + method);
            }
            return new Pair<>((Class<?>) parameterizedType.getActualTypeArguments()[0],
                    (Class<?>) parameterizedType.getActualTypeArguments()[1]);
        }
        return new Pair<>(null, null);
    }


    /**
     * 将查询结果映射成Map,其中第一个字段作为key,第二个字段作为value.
     *
     * @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
     * @param kvTypePair          函数指定返回Map key-value的类型
     */
    private Object result2Map(Statement statement, TypeHandlerRegistry typeHandlerRegistry, Pair<Class<?>, Class<?>> kvTypePair, CustomMap customMap) throws Throwable {
        ResultSet resultSet = statement.getResultSet();
        List<Object> res = new ArrayList<>();
        Map<Object, Object> map = new HashMap<>();
        while (resultSet.next()) {
            Object key = this.getObject(resultSet, 1, typeHandlerRegistry, kvTypePair.getKey());
            Object value = this.getObject(resultSet, 2, typeHandlerRegistry, kvTypePair.getValue());
            if (map.containsKey(key)) {// 该key已存在
                if (!customMap.isAllowKeyRepeat()) {
                    // 判断是否允许key重复
                    throw new DuplicateKeyException("CustomMap duplicated key!key=" + key);
                }
                Object preValue = map.get(key);
                if (!customMap.isAllowValueDifferentWithSameKey() && !Objects.equals(value, preValue)) {
                    // 判断是否允许value不同
                    throw new DuplicateKeyException("CustomMap different value with same key!key=" + key + ",value1=" + preValue + ",value2=" + value);
                }
            }
            // 第一列作为key,第二列作为value。
            map.put(key, value);
        }
        res.add(map);
        return res;
    }

    /**
     * 结果类型转换。
     * 这里借用注册在MyBatis的typeHander(包括自定义的),方便进行类型转换。
     *
     * @param columnIndex         字段下标,从1开始
     * @param typeHandlerRegistry MyBatis里typeHandler的注册器,方便转换成用户指定的结果类型
     * @param javaType            要转换的Java类型
     */
    private Object getObject(ResultSet resultSet, int columnIndex, TypeHandlerRegistry typeHandlerRegistry, Class<?> javaType) throws SQLException {
        final TypeHandler<?> typeHandler = typeHandlerRegistry.hasTypeHandler(javaType) ? typeHandlerRegistry.getTypeHandler(javaType) : typeHandlerRegistry.getUnknownTypeHandler();
        return typeHandler.getResult(resultSet, columnIndex);
    }

}

SqlSessionFactory配置拦截器
@Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        CatMybatisPlugin catMybatisPlugin = new CatMybatisPlugin();
        factoryBean.setPlugins(new Interceptor[]{catMybatisPlugin});
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
        factoryBean.setMapperLocations(resources);
        //设置自定义map拦截器
        factoryBean.setPlugins(new CustomMapResultPlugin());
        SqlSessionFactory sessionFactory = factoryBean.getObject();
        return sessionFactory;
    }
    
Mapper配置自定义注解
    @CustomMap
    Map<String, Long> queryTotalByType(Map<String, Object> map);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值