自定义mybatis TypeHandler

TypeHandler 说明

TypeHandler 是mybatis中重要的一个接口,定义了 java 类型的对象在和数据库交互的时候如果设置参数和从读取到的结果中获取值。

并且已经将常见的类型的处理器默认内置了,在插入和读取数据的时候会根据对应的数据类型找到对应的类型处理器。

mybatis自定义TypeHandler

对于一些非基本数据类型的字段,比如一个自定义类作为数据对象的一个属性,并且希望以特殊的格式映射到数据库中,在读取的时候能读取成java中的数据类型。用内置的typeHanlder 是一定不能满足需求的,
这就需要对typeHandler进行拓展。

最常见的例子就是将一个对象的某些数据以json序列化的方式存储在数据库中,读取出来的时候能够反序列化对应的java bean。

  1. 实体对象User

其中有2个自定义属性,都希望能够使用json的方式解析和转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class User {

private Long id;

private String name;

private Address address;

private Info info;

}


public class Address {

private String province;

private String city;

private Integer code;

private Double longitude;

private Double latitude;

}



public class Info {

private String message;

}

  1. 自定义一个typeHandler 类,继承 BaseTypeHandler,并实现对应的4个方法。
    主要是设置参数和获取结果值的方式的自定义实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

public class JsonTypeHandler<T> extends BaseTypeHandler<T> {

private ObjectMapper objectMapper = new ObjectMapper();

private Class<T> clzType;

public JsonTypeHandler(Class<T> type) {
this.clzType = type;
}

public JsonTypeHandler() {

}


@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, objectToJson(parameter));
}

@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return jsonToObject(rs.getString(columnName));
}

@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return jsonToObject(rs.getString(columnIndex));
}

@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return jsonToObject(cs.getString(columnIndex));
}

private T jsonToObject(String strValue){
if(StringUtils.isEmpty(strValue)){
return null;
}
try {
return objectMapper.readValue(strValue, clzType);
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage());
}
}


private String objectToJson(T param) {
if (param == null) {
return null;
}
try {
return objectMapper.writeValueAsString(param);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new IllegalArgumentException(e.getMessage());
}
}
}

  1. 现在typeHandler 已经定义好了,下面要做的是怎么在插入数据和查询结果的时候能够使用自定义的typeHandler.

在编写mapper文件的时候,resultMap.result 可以指定对应的typeHandler,在输入参数的时候也可以指定typeHandler

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="baseResultMap" type="com.demo.entity.User" >
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="address" jdbcType="VARCHAR" property="address" typeHandler="com.demo.conf.type_handlers.JsonTypeHandler" />
<result column="info" jdbcType="VARCHAR" property="info" typeHandler="com.demo.conf.type_handlers.JsonTypeHandler" />
</resultMap>


<insert id="saveUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into `user`(`name`,address,info) values (#{name},#{address,typeHandler=com.demo.conf.type_handlers.JsonTypeHandler},#{info,typeHandler=com.demo.conf.type_handlers.JsonTypeHandler});
</insert>

通过这种方式就完成了对应的类型和typeHanlder的一种 强制绑定

  1. 编写测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

@Test
public void save(){
User user =new User();
user.setName("33");
Address address = new Address();
address.setProvince("北京");
address.setCity("北京");
address.setCode(110000);
address.setLongitude(115.25);
address.setLatitude(39.26);

Info info = new Info();
info.setMessage(UUID.randomUUID().toString());

user.setAddress(address);
user.setInfo(info);
int count = userMapper.saveUser(user);

System.out.println(user.getId());

List<User> users = userMapper.selectAll();
System.out.println(users);
}

数据库里已经成功插入json数据并且也能正常读取。

  1. 上面的那种方式虽然能够完成要求,但是需要在每个mapper.xml 文件中写死对应的handler,需要配置一个统一全局的配置。
1
2
3
@MappedTypes(value = {Address.class, Info.class})
@MappedJdbcTypes(value = {JdbcType.VARCHAR,JdbcType.VARCHAR})
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {

这里用到2个注解:
@MappedTypes 表示那些数据类型应用此handler
@MappedJdbcTypes 表示对应的数据库类型

1
2
3
4
5
6
7
8
9
10
11
12

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = pathMatchingResourcePatternResolver.getResources("classpath:mappers/com/demo/mapper/*.xml");
factoryBean.setMapperLocations(resources);
factoryBean.setTypeHandlersPackage("com.demo.conf.type_handlers");
return factoryBean.getObject();
}

删除对应的mapper.xml 文件中指定的typehandler

1
2
3
4
5
6
7
8
9
10
11
12
13
    <resultMap id="baseResultMap" type="com.demo.entity.User" >
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<!-- <result column="address" jdbcType="VARCHAR" property="address" typeHandler="com.demo.conf.type_handlers.JsonTypeHandler" />-->
<!-- <result column="info" jdbcType="VARCHAR" property="info" typeHandler="com.demo.conf.type_handlers.JsonTypeHandler" /> -->
<result column="address" jdbcType="VARCHAR" property="address" />
<result column="info" jdbcType="VARCHAR" property="info" />
</resultMap>

<insert id="saveUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into `user`(`name`,address,info) values (#{name},#{address},#{info});
</insert>

指定一个TypeHandlersPackage,表示从哪个包中扫描初始化和注册对应的handler。如果handler比较少,也可以直接添加对一个的一个或几个handler到SqlSessionFactoryBean 上。

当添加了这个配置后,在初始化的时候会从指定的包下扫描typehanlder,并且获取typehanlder上的注解信息,为对应的自定义类型注册typehandler。
在插入数据或查询的时候解析到对应的类型的时候就能够找到特定的typehanlder进行处理了。

mybatis-plus自定义TypeHandler

在使用mybatis-plus 的时候,本身mybatis-plus在mybatis 的基础上增加了很多拓展和增强,使我们使用起来更加简单。

对于mybatis-plus 中为某个字段添加 typehanlder 只需要添加一个注解

1
2
@TableField(typeHandler = JsonTypeHandler.class)
private Address address;

这样就完成了,使用自定义的类型。
而且它本身也自带了很多常用的拓展类型

这些类型直接使用既可,有符号需求的都不需要自己定义。

TypeHandler 流程

TypeHandlerRegistry

跟TypeHanlder 相关的一个重要的类是 TypeHandlerRegistry。

顾名思义此类是维护和管理typeHandler的。

可以看到此类中有需要的map 映射关系,那么在系统初始化的时候会将 数据类型 和 类型处理器 做一个映射绑定。

在其构造方法上可以看到有 许多的默认类型注册。

都调用的是一个 publicvoid register(ClassjavaType, TypeHandler<? extends T> typeHandler) {的注册方法,相当于把一个javaType 和 typeHandler 做绑定。

系统初始化

在系统初始化的时候,会实例化 SqlSessionFactoryBean

SqlSessionFactoryBean#buildSqlSessionFactory 方法中有大量的初始化操作。

其中有这么两处代码。
如果有设置 typeHandlersPackage ,那么将从此包下扫描TypeHandler的子类,并且最终调用TypeHandlerRegistry 的 register 方法。

如果有单独设置 typeHandlers ,那么将循环注册这几个typeHandlers

如果既没有指定typeHandlersPackage 也没有指定typeHandlers,但是在mapper.xml 中写死了TypeHandler为什么也可以呢。

查看 XMLMapperBuilder#buildResultMappingFromContext 方法 既解析mapper xml的相关方法。

会调用 builderAssistant.buildResultMapping

resolveTypeHandler 的逻辑是

优先从 typeHandlerRegistry 中获取已经注册了的。如果没有注册的,那么通过 typeHandlerRegistry.getInstance 获取一个新的实例。所以这就是为什么直接写死而不配置也能生效的原因。

数据插入的时候

在为sql设置值的时候会调用DefaultParameterHandler#setParameters 方法

会循环参数依次为sql设置参数。

TypeHandler typeHandler = parameterMapping.getTypeHandler()

获取的是直接写在参数上的TypeHandler,如果没有写那么获取的是UnknownTypeHandler

最后调用对应的TypeHandler 的 setParameter

如果未获取到执行到 UnknownTypeHandler#setNonNullParameter

可以发现里面会再从一个获取。

根据参数的类型和jdbcType 从typeHandlerRegistry 中获取注册的类型。

这样就执行到自定义的TypeHanlder 的set参数方法.

数据读取的时候

获取数据处理查询结果的时候会调用DefaultResultSetHandler#getPropertyMappingValue

同样的 propertyMapping.getTypeHandler() 是直接写在resultMap 属性上的 typeHandler,如果没有写死那么就是 UnknownTypeHandler

在UnknownTypeHandler 的getNullableResult 同样做了查找TypeHandler 的处理。

这样也做到了优先获取resultMap 上的写死的TypeHandler,如果没有那么再根据类型去从typeHandlerRegistry 中查找。

最终调用getResult 方法。