浅谈MyBatis中@MapKey的妙用

目录
  • MyBatis @MapKey的妙用
    • 背景
    • 实现
    • 源码分析
    • 思考
  • Mybatis @MapKey分析
    • 1. MapKey注解有啥功能
    • 2. MapKey的源码分析
      • 1. MapperMethod对MapKey的操作
      • 2. DefaultMapResultHandler是什么

MyBatis @MapKey的妙用

背景

在实际开发中,有一些场景需要我们返回主键或者唯一键为Key、Entity为Value的Map集合,如Map<Long, User>,之后我们就可以直接通过map.get(key)的方式来获取Entity。

实现

MyBatis为我们提供了这种实现,Dao示例如下:

public interface UserDao {    
    @MapKey("id")
    Map<Long, User> selectByIdList(@Param("idList") List<Long> idList);    
}

需要注意的是:如果Mapper.xml中的select返回类型是List的元素,上面示例的话,resultType是User,因为selectMap查询首先是selectList,之后才是处理List。

源码分析

package org.apache.ibatis.session.defaults;
public class DefaultSqlSession implements SqlSession {
  ... ...
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
    final List<?> list = selectList(statement, parameter, rowBounds);
    final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
        configuration.getObjectFactory(), configuration.getObjectWrapperFactory());
    final DefaultResultContext context = new DefaultResultContext();
    for (Object o : list) {
      context.nextResultObject(o);
      mapResultHandler.handleResult(context);
    }
    Map<K, V> selectedMap = mapResultHandler.getMappedResults();
    return selectedMap;
  }  
  ... ...
}

selectMap方法其实是在selectList后的进一步处理,通过mapKey获取DefaultMapResultHandler类型的结果处理器,然后遍历list,调用handler的handleResult把每个结果处理后放到map中,最后返回map。

package org.apache.ibatis.executor.result;
public class DefaultMapResultHandler<K, V> implements ResultHandler {
  private final Map<K, V> mappedResults;  
  ... ...
  public void handleResult(ResultContext context) {
    // TODO is that assignment always true?
    final V value = (V) context.getResultObject();
    final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory);
    // TODO is that assignment always true?
    final K key = (K) mo.getValue(mapKey);
    mappedResults.put(key, value);
  }
  ... ...  
}

可以看出DefaultMapResultHandler是通过mapKey从元数据中获取K,然后mappedResults.put(key, value)放到map中。

思考

@MapKey这种处理是在查询完后做的处理,实际上我们也可以自己写逻辑将List转成Map,一个Lambda表达式搞定,如下:

  List<User> list = userDao.selectByIdList(Arrays.asList(1,2,3));
  Map<Integer, User> map = list.stream().collect(Collectors.toMap(User::getId, user -> user));

Mybatis @MapKey分析

先上例子

 @Test
  public void testShouSelectStudentUsingMapperClass(){
    //waring 就使用他作为第一个测试类
    try (SqlSession session = sqlMapper.openSession()) {
      StudentMapper mapper = session.getMapper(StudentMapper.class);
      System.out.println(mapper.listStudentByIds(new int[]{1,2,3}));
    }
  }
  @MapKey("id")
  Map<Integer,StudentDo> listStudentByIds(int[] ids);
  <select id="listStudentByIds" resultType="java.util.Map">
    select  * from t_student
    where  id in
      <foreach collection="array" open="(" separator="," close=")" item="item">
            #{item}
      </foreach>
  </select>

结果

1. MapKey注解有啥功能

Mapkey可以让查询的结果组装成Map,Map的key是@MapKey指定的字段,Value是实体类。如上图所示

2. MapKey的源码分析

还是从源码分析一下他是怎么实现的,要注意MapKey不是写在Xml中的,而是标注在方法上的。所以,对于XML文件来说,他肯定不是在解析文件的时候操作的。对于Mapper注解实现来说,理论上来说是在解析的时候用的,但是对比XML的解析来说,应该不是。多说一点,想想Spring ,开始的时候都是XML,最后采用的注解,并且注解的功能和XML的对应起来,所以在解析XML是怎么解析的,在解析注解的时候就应该是怎么解析的。

还是老套路,点点看看,看看哪里引用到了他,从下图看到,用到的地方一个是MapperMethod,一个是MapperAnnotationBuilder,在MapperAnnotationBuilder里面只是判断了一下,没有啥实质性的操作,这里就不用管。只看前者。

1. MapperMethod对MapKey的操作

看过之前文章的肯定知道,MapperMethod是在哪里创建的。这个是在调用mapper接口的查询的时候创建的。接口方法的执行最终会调用到这个对象的execute方法

MapperMethod里面包含两个对象SqlCommand和MethodSignature,就是在MethodSignature里面引用了MapKey的

 public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
     //判断此方法的返回值的类型
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
     // 返回值是否为空
      this.returnsVoid = void.class.equals(this.returnType);
     // 返回是否是一个列表或者数组
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      // 返回值是否返回一个游标
      this.returnsCursor = Cursor.class.equals(this.returnType);

    // 返回值是否是一个Optional
      this.returnsOptional = Optional.class.equals(this.returnType);

     // 重点来了,这里会判断返回值是一个MapKey,并且会将MapKey里Value的值赋值给MapKey
      this.mapKey = getMapKey(method);

     // 返回值是否是一个map
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); //找到方法参数里面 第一个 参数类型为ResultHandler的值
      // 这里是处理方法参数里面的param注解, 注意方法参数里面有两个特殊的参数 RowBounds和 ResultHandler
      // 这里会判断@param指定的参数,并且会将这些参数组成一个map,key是下标,value是param指定的参数,如果没有,就使用方法参数名
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

上面的代码这次最重要的是mapKey的赋值操作getMapKey,来看看他是什么样子

private String getMapKey(Method method) {
      String mapKey = null;
      if (Map.class.isAssignableFrom(method.getReturnType())) {
        final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
        if (mapKeyAnnotation != null) {
          mapKey = mapKeyAnnotation.value();
        }
      }
      return mapKey;
    }

上面介绍了MapKey是在哪里解析的,下面分析Mapkey是怎么应用的,抛开所有的不说,围绕查询来说。经过上面的介绍。已经对查询的流程很清晰了,因为查询还是普通的查询,所以,MapKey在组装值的时候才会发送作用,下面就看看吧

还是老套路,既然赋值给MethodSignature的mapKey了,点点看看,哪里引用了他

下面的没有啥可看的,看看上面,在MapperMethod里面用到了,那就看看

//看这个名字就能知道,这是一个执行Map查询的操作
private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
    Map<K, V> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      // 将Map传递给sqlSession了,那就一直往下走
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
    } else {
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
    }
    return result;
  }

一直点下去,就看到下面的这个了,可以看到,这里将mapKey传递给了DefaultMapResultHandler,对查询的结果进行处理。

  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
     //这已经做了查询了
    final List<? extends V> list = selectList(statement, parameter, rowBounds);
    final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
            configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
    final DefaultResultContext<V> context = new DefaultResultContext<>();
    // 遍历list,利用mapResultHandler处理List
    for (V o : list) {
      context.nextResultObject(o);
      mapResultHandler.handleResult(context);
    }
    return mapResultHandler.getMappedResults();
  }

这里很明确了,先做正常的查询,在对查询到的结果做处理(DefaultMapResultHandler)。

2. DefaultMapResultHandler是什么

/**
 * @author Clinton Begin
 */
public class DefaultMapResultHandler<K, V> implements ResultHandler<V> {
  private final Map<K, V> mappedResults;
  private final String mapKey;
  private final ObjectFactory objectFactory;
  private final ObjectWrapperFactory objectWrapperFactory;
  private final ReflectorFactory reflectorFactory;
  @SuppressWarnings("unchecked")
  public DefaultMapResultHandler(String mapKey, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
    this.objectFactory = objectFactory;
    this.objectWrapperFactory = objectWrapperFactory;
    this.reflectorFactory = reflectorFactory;
    this.mappedResults = objectFactory.create(Map.class);
    this.mapKey = mapKey;
  }
 // 逻辑就是这里,
  @Override
  public void handleResult(ResultContext<? extends V> context) {
    //拿到遍历的list的当前值。
    final V value = context.getResultObject();
    //构建MetaObject,
    final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
    // TODO is that assignment always true?
    // 获取mapKey指定的属性,放在mappedResults里面。
    final K key = (K) mo.getValue(mapKey);
    mappedResults.put(key, value);
  }
 // 返回结果
  public Map<K, V> getMappedResults() {
    return mappedResults;
  }
}

这里的逻辑很清晰,对查询查到的list。做遍历,利用反射获取MapKey指定的字段,并且组成Map,放在一个Map(mappedResults,这默认就是HashMap)里面。

问题?

1, 从结果中获取mapkey字段的操作,这个字段总是有的吗?

不一定,看这个例子,MapKey是一个不存在的属性值,那么在Map里面就会存在一个Null,这是Hashmap决定的。

综述:

在MapKey的使用中,要注意MapKey中Value字段的唯一性,否则就会造成Key值覆盖的操作。同时也要注意,Key要肯定存在,否则结果就是null,(如果有特殊操作的话,就另说)话说回来,这里我觉得应该增加强校验。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • mybatis实现遍历Map的key和value

    目录 mybatis 遍历Map的key和value sql.xml java代码 foreach嵌套遍历Map的key和value 具体做法:(Oracle数据库) 使用 内层循环中使用 mybatis 遍历Map的key和value sql.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0

  • Mybatis中注解@MapKey的使用详解

    mybatis的原身是ibatis,现在已经脱离了apache基金会,新官网是http://www.mybatis.org/. 在研究Mybatis源码之前并不知道这个注解的妙用的,但是当我看到参数解析的时候 有这个一个注解,所以我了解了一下,当我们返回像Map<String, Map<String, Object>>这种类型的时候,我们往往很难做到,因为这里面可能是多个表的数据,所以我们不可能再建一个模型. 这时候我们就可以使用这个注解了 @Retention(Retention

  • mybatis返回map结果集@MapKey使用的场景分析

    目录 mybatis返回map结果集@MapKey使用场景 使用id作为map的ke Map的value为Map,一条记录对应一个Map 使用name作为map的key mybatis使用@MapKey注解 背景和含义 具体示例 mybatis返回map结果集@MapKey使用场景 select的 resultType属性为map时: 通过MapKey指定map的key值 使用id作为map的ke @MapKey("id") Map<Long, UserInfo> getU

  • 浅谈MyBatis中@MapKey的妙用

    目录 MyBatis @MapKey的妙用 背景 实现 源码分析 思考 Mybatis @MapKey分析 1. MapKey注解有啥功能 2. MapKey的源码分析 1. MapperMethod对MapKey的操作 2. DefaultMapResultHandler是什么 MyBatis @MapKey的妙用 背景 在实际开发中,有一些场景需要我们返回主键或者唯一键为Key.Entity为Value的Map集合,如Map<Long, User>,之后我们就可以直接通过map.get(k

  • 浅谈mybatis中的#和$的区别 以及防止sql注入的方法

    mybatis中的#和$的区别 1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id,  如果传入的

  • 浅谈mybatis中的#和$的区别

    1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为ord

  • 浅谈mybatis中SQL语句给boolean类型赋值问题

    我就废话不多说了,大家还是直接看代码吧~ <select id="getBiTree" parameterType="String" resultMap="MenuVoListMap"> SELECT m.menu_id , m.parent_id , m.`name` , 1 opens FROM menu m WHERE m.is_valid = 1 AND (m.type = 0 or m.type = 1) and m.men

  • 浅谈Mybatis中resultType为hashmap的情况

    现在有一张user表 id ,name,age 我们进行一个简单的查询: <select id="test" resultType="Uer"> select id ,name,age from user </select> 查询完后,怎么去接收这个查询结果呢,通常在这个mapper.xml对应的接口中使用List<User>做为返回值去接收,最后存储的样子就是下面的图 这是一个很简单的单表查询操作,其实这种简单的单表查询操作不需

  • 浅谈myBatis中的插件机制

    插件的配置与使用 在mybatis-config.xml配置文件中配置plugin结点,比如配置一个自定义的日志插件LogInterceptor和一个开源的分页插件PageInterceptor: <plugins> <plugin interceptor="com.crx.plugindemo.LogInterceptor"></plugin> <plugin interceptor="com.github.pagehelper.P

  • 浅谈mybatis mapper.xml文件中$和#的区别

    #{}表示一个占位符即?,可以有效防止sql注入.在使用时不需要关心参数值的类型,mybatis会自动进行java类型和jdbc类型的转换. #{}可以接收简单类型值或pojo属性值,如果传入简单类型值,#{}括号中可以是任意名称. <!-- 根据名称模糊查询用户信息 --> <select id="findUserById" parameterType="String" resultType="user"> select

  • 浅谈JavaScript中的parseInt()的妙用

    起因 写这篇博客的起因是今天在刷leetcode的每日一题,是一道字符串转换整数 (atoi)的题,感兴趣的话可以点击题目名称去看一下具体描述.在我多次debug终于成功提交之后,去评论区看了一下大佬们的解题思路,看完之后不禁感叹javascript中原来parseInt( )已经这么优秀了啊.这告诉我了一个道理,我们自认为再熟悉不过的api,可能我们并没有真正意义上的掌握." 我的解答 /** * @param {string} str * @return {number} */ var my

  • 浅谈Mybatis版本升级踩坑及背后原理分析

    1.背景 某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.1. 当服务开始上线后,开始陆续出现了一些更新系统交互日志方面的报警,属于系统辅助流程,报警下图所示, 具体系统数据已脱敏,内容是Mybatis相关的报警,在进行类型转换的时候,产生了强转错误. 更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"call

  • 浅谈mybatis 乐观锁实现,解决并发问题

    情景展示: 银行两操作员同时操作同一账户就是典型的例子. 比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交.最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050.这就是典型的并发问题. 乐观锁机制在一定程度上解决了这个问题.乐观锁,大多是基于数据版本(Version)记录机制实现.何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "

随机推荐