浅谈MyBatis中@MapKey的妙用
作者:totally123 发布时间:2023-09-25 10:42:02
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,(如果有特殊操作的话,就另说)话说回来,这里我觉得应该增加强校验。
来源:https://blog.csdn.net/totally123/article/details/102723732
猜你喜欢
- 一、连接数据库的配置单独放在一个properties文件中之前,我们是直接将数据库的连接配置信息写在了MyBatis的conf.xml文件中
- List去重复,我们首先想到的可能是 利用List转Set集合,因为Set集合不允许重复。所以达到这个目的。 如果集合里面是简单对
- Kryo框架的source已移至https://github.com/EsotericSoftware/kryo ,进入此页面,然后点击右边
- 关于页面渲染其实在工作中,一直都是前后端分离,也就是说,我的工作从来都是提供接口,而不写 html css js 之类的,所以在这方面也没有
- 目录一:spring读取配置或注解的过程二:spring的bean的生命周期2.1:实例化 Instantiation2.2:初始化3: 使
- 异常处理机制已经成为判断一门编程语言是否成熟的标准之一,其对代码的健壮性有很大影响。一直以来异常处理使用不是很得心应手,今天对异常进行了较为
- 单独使用mybatis是有很多限制的(比如无法实现跨越多个session的事务),而且很多业务系统本来就是使用spring来管理的事务,因此
- 概述使用this()或target()可绑定被代理对象实例,在通过类实例名绑定对象时,还依然具有原来连接点匹配的功能,只不过类名是通过增强方
- 本文所述实例实现将一张图片上传到指定的文件夹,然后在窗体上的PictrueBox控件中显示出来。具体功能代码如下:private void
- 一、串口连接的打开与关闭串口,即COM口,在.NET中使用 SerialPort 类进行操作。串口开启与关闭,是涉及慢速硬件的IO操作,频繁
- 前言最近有个网友问了我一个问题:系统中大事务问题要如何处理?正好前段时间我在公司处理过这个问题,我们当时由于项目初期时间比较紧张,为了快速完
- 本文实例为大家分享了java实现图片分割指定大小的具体代码,供大家参考,具体内容如下1.使用工具:ThumbnailsThumbnails
- MyBatis框架提供了二级缓存接口,我们只需要实现它再开启配置就可以使用了。特别注意,我们要解决缓存穿透、缓存穿透和缓存雪崩的问题,同时也
- 本文实例为大家分享了java读取cvs文件并导入数据库的具体代码,供大家参考,具体内容如下首先获取文件夹下面的所有类型相同的excel,可以
- 转发和重定向相同点都是web开发中资源跳转的方式。不同点转发:是服务器内部的跳转,浏览器的地址栏不会发生变化。从一个页面到另一个页面的跳转还
- 成员内部类1.定义成员内部类是直接定义在类中,不加任何修饰符的(特指不加static修饰的)的内部类,可以类比着成员变量来理解,如下面这个代
- 一、ArrayList简介在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:ArrayList底层是一段连
- SpringCloud简介Spring cloud是一个基于Spring Boot实现的服务治理工具包,在微服务架构中用于管理和协调服务的微
- Android手势解锁本文讲述的是一个手势解锁的库,可以定制显示隐藏宫格点、路径、并且带有小九宫格显示图,和震动!让你学会使用这个简单,高效
- RocketMQ 是什么Github 上关于 RocketMQ 的介绍:RcoketMQ 是一款低延迟、高可靠、可伸缩、易于使用的消息中间件