浅谈MyBatis中@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>
结果
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,(如果有特殊操作的话,就另说)话说回来,这里我觉得应该增加强校验。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。