浅谈MyBatis中@MapKey的妙用

2022-07-18,,,

目录
  • 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,(如果有特殊操作的话,就另说)话说回来,这里我觉得应该增加强校验。

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

《浅谈MyBatis中@MapKey的妙用.doc》

下载本文的Word格式文档,以方便收藏与打印。