在Mybatis Plus中使用TypeHandler映射PostgreSQL内的JSON字段
业务需求
在PostgreSQL数据库表中有两个JSON类型的字段,字段中的数据都是以JSON数组形式储存的 [{},{}]
1 | create table problem |
在使用MybatisPlus时也需要把这两个字段映射为对象。最简单的实现方法就是把它映射成String
类型,并在之后的业务代码中手动进行序列化和反序列化的操作。
但是很显然,这样做太不优雅了。我想要把JSON字段直接映射成对应实体类的类型。这样我们便不必关注于序列化、反序列化操作,而是面向实体类对象操作。
TypeHandler
简介
Mybatis提供了TypeHandler这一组件,用于处理Java对象与数据库列之间的类型转换。当MyBatis从数据库中读取数据时,需要将数据库中的原始数据转换为Java对象,或者在将Java对象写入数据库之前将其转换为数据库支持的数据类型。MyBatis提供了一些默认的TypeHandler用于处理常见的Java类型与数据库列之间的类型转换。这些默认的TypeHandler包含在MyBatis的核心库中,无需额外配置即可使用。比如IntegerTypeHandler
、LongTypeHandler
等等
但是如果我们使用的Java对象使用的类型不在默认TypeHandler的范围内(比如我们自定义的实体类)。则需要我们编写自定义的TypeHandler来处理特定类型的转换。TypeHandler必须实现org.apache.ibatis.type.TypeHandler
接口来实现。TypeHandler
核心方法是setParameter
和getResult
. 在设置参数时(将Java对象写入数据库),MyBatis会调用TypeHandler的setParameter
方法,而在获取结果时(从数据库中读取数据),MyBatis会调用getResult
方法。
BaseTypeHandler
我们在自定义TypeHandler时不必直接实现org.apache.ibatis.type.TypeHandler
接口,框架本身提供了BaseTypeHandler这一抽象类。它的继承关系是这样的。
1 | public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> |
它使用了 模板方法 这一设计模式。
模板方法(Template Method)是一种行为设计模式,它定义了一个算法的骨架,将一些步骤的实现推迟到子类中。该模式允许子类在不改变算法结构的情况下,重新定义特定步骤的实现,从而在同一个算法框架下可以有不同的实现方式。
- 定义一个抽象类,该抽象类包含称为“模板方法”的方法,它定义了算法的骨架,其中包含了多个步骤或操作。
- 在抽象类中,将一些步骤的具体实现留给子类来完成,这些步骤用抽象方法声明或默认实现。
- 子类通过继承抽象类并实现其中的抽象方法来自定义特定步骤的实现,从而完成整个算法的定制。
针对于TypeHandler接口中的方法,它是这样实现的(我删除了源码中相当多的异常处理的代码,逻辑更加清晰)。
1 |
|
可以看到,它们被编写成了模板方法。它们分别调用了setNonNullParameter
,getNullableResult
,getNullableResult
,getNullableResult
这几个方法,而这几个方法也是抽象方法,需要由子类去实现。
1 | public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; |
这样做的好处是BaseTypeHandler
封装了处理Java对象与数据库列之间类型转换的通用逻辑,包括处理null
值、抛出异常等等。我们就无需在每个自定义TypeHandler中重复实现相同的逻辑。有兴趣的话可以翻阅一下这几个模板方法的源码,比我贴出来的要复杂很多(有相当多的异常处理的代码)要是让我每次自定TypeHandler都写一遍这个异常处理逻辑,那不如要我的命- -。提高了代码的可维护性和可读性。
而且它也继承了TypeReference类,这是解决泛型擦除问题:Java中的泛型在编译时会进行类型擦除,这意味着在运行时无法直接获取泛型类型的信息。
TypeReference
通过继承方式,帮助TypeHandler获取泛型类型的实际信息。因为TypeReference
本身是一个带有泛型参数的类,它可以保留泛型信息。这样做可以帮助Mybatis在运行时准确的指定实体类使用哪个TypeHandler
实现步骤
这次的需求中我们需要把test
字段和solution
字段映射成实体类,所以我们需要根据JSON的具体数据格式来创建两个实体类**Test
和Solution
,然后再为这两个实体类分别自定义各自的TypeHandler。然后在数据库表实体类中Test
和Solution
**这两个成员变量上,通过@TableField(typeHandler = xxx.class)
指定它对应的TypeHandler。最后再通过TypeHandlerRegistry完成TypeHandler的注册。
创建实体类
1 |
|
1 |
|
创建对应的TypeHandler
创建AbstractJsonTypeHandler
因为这两个数据转换操作都是JSON字符串的序列化和反序列化操作,有着通用的逻辑。我们也可以使用上面提到的模板方法这一设计模式来简化开发。于是我创建了AbstractJsonTypeHandler,把原有的四个方法全定义成模板方法,这样我们只需要在子类中关注如何把对应的JSON字符串转为对应泛型的类型就行了。
2023-08-06 :这个方法其实很蠢,
parseJsonString
也是一个通用操作,根本不需要定义模板方法,而是直接实现就行,这样可以定义一个通用的JSON字段类型的TypeHandler(并且其实Mybatis-Plus提供了有现成的**FastJsonTypeHandler
和JacksonTypeHandler
)我这里其实是自己做了一个方形的轮子**。现在已经换了更好的实现,可以查看另一篇TypeHandler的博文。这次自己的实现也并非是无意义的,它让我进一步了解了TypeHandler的设计思想和工作原理。
1 | public abstract class AbstractJsonTypeHandler<T> extends BaseTypeHandler<T> { |
之后再根据不同的实体类为它们创建各自的TypeHandler。
1 |
|
1 |
|
注册:
1 |
|
指定:
1 |
|
- 实际上生效的只有注册的操作,要想使
@TableField(typeHandler = FastjsonTypeHandler.class)
生效,必须要在这个实体类上标注@TableName(value = "problem",autoResultMap = true)
让autoResultMap = true。否则MybatisPlus无法推断**Test[]和Solution[]**的类型。导致注解失效,这个会在另一篇博客中展开说一下。 - 基于上一点,这两步做一个就可以了,在标注注解后,MyabtisPlus会帮我们注册。或者我们手动注册。
为什么是数组和非集合
还是因为泛型擦除问题,如果使用集合List<Test>
,List<Solution>
,那么在Mybatis进行类型推断时,会丢失泛型,认为这两个都是单纯的List类型。从而导致无法正确的指定它们各自的TypeHandler,针对这个问题,我也提了一个 Issue 。并没有找到更好的解决方案,于是就用数组形式,从而避免使用泛型。
至此,就已经完成了实体类对象到JSON类型字段的映射。