欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅谈MyBatis中@MapKey的妙用

程序员文章站 2022-03-13 18:52:11
目录mybatis @mapkey的妙用背景实现源码分析思考mybatis @mapkey分析1. mapkey注解有啥功能2. mapkey的源码分析1. mappermethod对mapkey的操...

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>

结果

浅谈MyBatis中@MapKey的妙用

1. mapkey注解有啥功能

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

2. mapkey的源码分析

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

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

浅谈MyBatis中@MapKey的妙用

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了,点点看看,哪里引用了他

浅谈MyBatis中@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字段的操作,这个字段总是有的吗?

浅谈MyBatis中@MapKey的妙用

不一定,看这个例子,mapkey是一个不存在的属性值,那么在map里面就会存在一个null,这是hashmap决定的。

综述:

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

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

相关标签: MyBatis @MapKey