自定义Mybatis的TypeHandler,轻松应对Mysql的JSON类型

引言

MyBatis中,TypeHandler是一个核心的组件,负责处理数据库字段与Java对象之间的类型转换。由于不同数据库系统和Java数据类型之间存在差异,因此需要TypeHandler来进行数据的转换,以确保数据的正确性和一致性。接下来我们一起探讨MyBatisTypeHandler的相关知识,包括其基础原理、编写自定义TypeHandler、处理常见数据类型、高级应用以及性能优化等内容。

TypeHandler的工作原理

TypeHandler 的工作原理主要体现在两个关键环节:参数设置和结果集映射。

  1. 参数设置 : 当MyBatis 执行SQL语句时,需要将用户传入的方法参数或者 Mapper XML文件中定义的参数值设置到PreparedStatement对象中。对于非基本类型的参数,如自定义对象、枚举或其他复杂类型,MyBatis将通过查找对应的 TypeHandler 实现类来完成转换工作。即MyBatis根据参数的Java类型找到对应的TypeHandler,然后调用其setParameter 方法,这个方法会将Java类型的数据转换为JDBC可识别的数据库类型,并调用PreparedStatementset方法将转换后的数据写入预编译的SQL语句中。
image.png

image.png

  1. 结果集映射 : 在查询执行完毕后,MyBatis需要将从ResultSet中读取的数据转换成Java类型并填充到目标对象属性上。如下即为根据jdbcType或者javaType获取对对应的typeHandler
image.png

image.png

如果在Mybatis的xml中指定了TypeHandler则会直接使用这个Handler。

MyBatis通过结果映射配置来确定将结果集中的哪些列映射到Java对象的哪些属性上。当MyBatisResultSet中获取某列数据时,它会根据结果映射配置所关联的Java类型,找到相应的TypeHandler。然后,MyBatis调用TypeHandlergetResult方法,该方法将从数据库返回的JDBC类型数据转换为Java类型,并最终赋值给目标Java对象的属性。这个过程确保了数据库中的数据与Java对象之间的正确转换和映射,从而实现了数据的持久化操作。

image.png

image.png

自定义TypeHandler

MyBatis中,虽然已经提供了丰富的内置TypeHandler来处理常见的数据类型,但在实际开发中,有时候我们可能需要处理一些特殊的数据类型或者定制化的数据转换逻辑,例如数据库中的某个字段存储的是特定格式的字符串(例如JSON数据类型),但Java端需要将其转换为枚举或自定义对象。这时候,就需要编写自定义的TypeHandler来进行数据处理。

TypeHandler接口

要编写自定义的TypeHandler,首先需要实现MyBatis提供的TypeHandler接口。该接口定义了处理结果集方法getResult和处理参数的方法setParameter

image.png

image.png

在实现TypeHandler接口后,我们需要重写这几个方法,setParameter方法用于将Java对象的属性值设置到PreparedStatement中,而getResult方法则用于将ResultSet中的数据转换为Java对象的属性值。

BaseTypeHandler抽象类

实际上,我们在日常开发中常使用的并不是实现TypeHandler接口,而是继承BaseTypeHandler抽象类。BaseTypeHandler实现了TypeHandler接口,BaseTypeHandler提供了对TypeHandler接口中方法的默认实现,包括空值处理、异常处理等,减少了重复的代码编写,提高了开发效率。同时,BaseTypeHandler也提供了一些扩展点,使得开发者可以在不改变核心逻辑的情况下进行定制化扩展,满足不同场景下的需求。使用BaseTypeHandler 抽象类可以使自定义TypeHandler的编写更加规范、简化和灵活。

image.png

image.png

MappedJdbcTypes和MappedTypes

在自定义的TypeHanlder时,我们也可以使用@MappedJdbcTypes@MappedTypes注解,显示的指定你的TypeHanlder要处理的JDBC类型和Java类型。

  • @MappedJdbcTypes注解用于指定该TypeHandler支持的JDBC类型。通过这个注解,可以明确告知Mybatis此类型处理器在getResult时应该处理哪些数据库中的数据类型(如VARCHARTIMESTAMP等)。
java 复制代码
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.VARBINARY})  
public class AddressToJsonBaseTypeHandler<T> extends BaseTypeHandler<AddressBO> {
    // 具体梳理逻辑
}

如上,TypeHandler将被用于映射到JDBC类型为VARCHAR的列上的AddressBO类型。

  • @MappedTypes注解则用于指定Java类型,它告诉Mybatis这个TypeHandler在执行setParameter时应该关联于哪些Java类或接口上。
csharp 复制代码
@MappedTypes({AddressBO.class})  
public class AddressToJsonBaseTypeHandler<T> extends BaseTypeHandler<AddressBO> {
    // 具体逻辑处理
}

如上,当Mybatis遇到与AddressBO类型相匹配的属性时,会使用这个TypeHandler进行转换。

通常,为了确保TypeHandler能在正确的地方被应用,同时考虑到可读性和维护性,推荐在自定义TypeHandler中同时使用这两个注解来清晰地定义其适用范围:

less 复制代码
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.VARBINARY})
@MappedTypes({AddressBO.class})  
public class AddressToJsonBaseTypeHandler<T> extends BaseTypeHandler<AddressBO> {
    // 具体逻辑处理
}

但是在某些情况下,如果是在全局配置或者具体的映射文件中显式制定注册了TypeHandler,则可能不需要这些注解也能正常工作。但是,使用注解可以提高代码的清晰度和自动化的可能性。

注册TypeHandler

编写完自定义的TypeHandler后,还需要将其注册到MyBatis的配置中(typeHandlerMap中),以便MyBatis能够正确地识别和使用它。

全局配置

我们可以在创建SqlSessionFactory时,通过SqlSessionFactoryBeansetTypeHandlers的方法全局指定你的TypeHandler

ini 复制代码
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();  
factoryBean.setDataSource(dataSource);   
factoryBean.setTypeHandlers(new AddressTypeHandler());

此时我们就可以在TypeHandlerMap中可以发现这个自定义的TypeHanlder了。

image.png

image.png

局部指定

如果针对某些特定的表下特定的字段(即不通用的字段)可以在该映射文件中使用该TypeHandler,可以在对应的XML映射文件中进行配置制定。

xml 复制代码
<!-- 在某个字段上指定TypeHandler -->
<resultMap id="BaseResultMap" type="com.springboot.mybatis.entity.UserInfoDO">  
  <id column="id"  property="id" />  
  <result column="user_name" jdbcType="VARCHAR" property="userName" />  
  <result column="address" jdbcType="VARCHAR" property="address" typeHandler="com.springboot.mybatis.handler.AddressTypeHandler"/>  
  <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />  
</resultMap>

<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">  
  select *  from user_info where id = #{id,jdbcType=BIGINT}
</select>

<!-- 或者在参数映射中指定 -->
<update id="updateByPrimaryKeySelective" parameterType="com.springboot.mybatis.entity.UserInfoDO">  
  <[email protected]>  
  update user_info  
  <set>  
    <if test="userName != null">  
      user_name = #{userName,jdbcType=VARCHAR},  
    </if>  
    <if test="address != null">  
      address = #{address,jdbcType=VARCHAR,typeHandler=com.springboot.mybatis.handler.AddressTypeHandler}, 
    </if>  
  </set>  
  where id = #{id,jdbcType=BIGINT}  
</update>

这样,当Mybatis执行SQL时,对于对应类型的数据就会自动调用你定义的TypeHandler来进行数据转换。

示例

我们以MysqlJSON数据类型为例,以《解锁Mysql的JSON数据类型》文中示例为例,我们查询以及保存user_info表中的address字段,因address字段在库中以JSON存储,我们在UserInfoDO中使用对象AddressBO接收。

我们定义一个专门处理数据JSON类型数据与Java对象相互转换的一个抽象的TypeHandler,它继承了BaseTypeHandler

java 复制代码
public abstract class JsonBaseTypeHandler<T> extends BaseTypeHandler<T> {  

    private static final ObjectMapper objectMapper;  

    static {  
        objectMapper = new ObjectMapper();  
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);  
        //使用null表示集合类型字段是时不抛异常  
        objectMapper.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);  
        //对象为空时不抛异常  
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);  
    }  

    /**  
     * json转换为obj  
     * @param json json串  
     * @return object  
     */    private T parse(String json) {  
        try {  
            if (StringUtils.isBlank(json)) {  
                return null;  
            }  
            return objectMapper.readValue(json, specificType());  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }  
    }  

    /**  
     * obj转换为json  
     * @param obj object对象  
     * @return json个数的字符串  
     */  
    private String toJsonString(T obj) {  
        if (obj == null){  
            return "";  
        }  
        try {  
            return objectMapper.writeValueAsString(obj);  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }  
    }  

    @Override  
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {  
        String content = parameter == null ? null : toJsonString(parameter);  
        ps.setString(i, content);  
    }  

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

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

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

    /**  
     * 具体类型,由子类提供  
     *  
     * @return 具体类型  
     */  
    protected abstract TypeReference<T> specificType();  

}

这样一个通用的JSONJava对象的通用Handler就完成了。然后我们具体的字段转换到相应的Java对象时,只需要继承这个抽象类,把Java对象传递过去即可。

csharp 复制代码
public class AddressTypeHandler extends JsonBaseTypeHandler<AddressBO> {  

    /**  
     * 具体类型,由子类提供  
     *  
     * @return 具体类型  
     */  
    @Override  
    protected TypeReference<AddressBO> specificType() {  
        return new TypeReference<AddressBO>() {};  
    }  
}

然后我们分别指定查询的ResultMap以及插入的sql中的address字段的TypeHandlerAddressTypeHandler的全路径。

sql 复制代码
<resultMap id="BaseResultMap" type="com.springboot.mybatis.entity.UserInfoDO">  
  <id column="id"  property="id" />  
  <result column="user_name" jdbcType="VARCHAR" property="userName" />  
  <result column="address" jdbcType="VARCHAR" property="address" typeHandler="com.springboot.mybatis.handler.AddressTypeHandler"/>  
  <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />  
</resultMap>

<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">  
  select *  from user_info where id = #{id,jdbcType=BIGINT}
</select>

<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.springboot.mybatis.entity.UserInfoDO" useGeneratedKeys="true">  
  <[email protected]>  
  insert into user_info (user_name, address, create_time  
    )  values (#{userName,jdbcType=VARCHAR},          #{address,jdbcType=VARCHAR,typeHandler=com.springboot.mybatis.handler.AddressTypeHandler},   
          #{createTime,jdbcType=TIMESTAMP}  
    )
</insert>

UserInfoDO中的address属性由String修改为AddressBO

ruby 复制代码
@Data  
public class UserInfoDO {  
    /**  
    * 自增主键  
    */  
    private Long id;  

    /**  
    * 名称  
    */  
    private String userName;  

    /**  
    * 地址信息  
    */  
    private AddressBO address;  

    /**  
    * 创建时间  
    */  
    private Date createTime;
}

然后我们分别执行插入以及查询的方法。

ini 复制代码
@Test  
public void insertUserTest(){  
    UserInfoDO userInfoDO = new UserInfoDO();  
    userInfoDO.setUserName("lisi");  
    AddressBO addressBO = new AddressBO();  
    addressBO.setCity("New York");  
    addressBO.setStreet("123 Main St");  
    addressBO.setCountry("US");  
    List<Integer> zipcodes = new ArrayList<>();  
    zipcodes.add(94507);  
    zipcodes.add(94582);  
    addressBO.setZipcodes(zipcodes);  
    userInfoDO.setAddress(addressBO);  
    userInfoDO.setCreateTime(new Date());  
    userInfoMapper.insert(userInfoDO);  
}

数据库中结果为:

image.png

image.png

然后我们在执行查询方法:

csharp 复制代码
@Test  
public void listUserTest(){  
    UserInfoDO userInfoDO = userInfoMapper.selectByPrimaryKey(6L);  
    System.out.println(userInfoDO);  
}

打印结果为:

image.png

image.png

MyBatis框架中,采用自定义TypeHandler实现JSON数据类型字段与Java对象的相互转换具有显著的优势。通过精心设计和实现TypeHandler,可以精准把控从Java对象到JSON字符串以及反向转换的过程,确保数据在存入数据库时按照预设格式可靠地序列化,并在读取时准确无误地还原为对应的Java实体,从而有效避免因数据格式不兼容引发的运行时异常或数据损坏问题。同时,利用TypeHandler将数据持久化的具体逻辑进行抽象封装,使业务代码得以聚焦核心功能,不受底层数据库交互细节的影响,极大提升了代码的可读性和维护性。而在整个项目范围内统一应用自定义的TypeHandler,有利于维持数据操作的一致性和标准化,消除了由于开发人员使用不同处理策略带来的潜在风险,有力推动了项目的整体开发效率和维护质量提升。

总结

本文介绍了MyBatisTypeHandler的概念、基础用法以及应用。我们探讨了如何编写自定义的 TypeHandler来处理特殊格式的数据。通过本文的学习,我们可以更好地理解和应用TypeHandler,在实际开发中处理数据库操作时能够更加灵活、高效地运用MyBatis框架。

在使用TypeHandler的过程中,我们需要注意数据的准确性和一致性,确保数据的正确转换和映射,避免出现数据丢失或者转换错误的情况。除了简单的数据类型转换外,TypeHandler还可以用于进行数据校验和转换。在TypeHandler的实现中,我们可以添加一些逻辑来对数据进行校验,如检查数据的有效性、范围等。通过在TypeHandler中添加数据校验和转换逻辑,我们可以确保数据的完整性和正确性。

另外,还需要根据具体的业务需求和数据特点来选择合适的 TypeHandler,并根据需要进行性能优化和调整,以提高系统的整体性能和稳定性。

通过对TypeHandler的学习和实践,我们可以更加灵活地处理各种数据类型和数据格式,为项目的顺利进行和未来的发展打下坚实的基础。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

相关推荐
用户675704988502几秒前
告别数据库瓶颈!用这个技巧让你的程序跑得飞快!
后端
千|寻18 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱32 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯38 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响41 分钟前
枚举在实际开发中的使用小Tips
后端
wuhunyu1 小时前
基于 langchain4j 的简易 RAG
后端
techzhi1 小时前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql