引言
在MyBatis
中,TypeHandler
是一个核心的组件,负责处理数据库字段与Java
对象之间的类型转换。由于不同数据库系统和Java
数据类型之间存在差异,因此需要TypeHandler
来进行数据的转换,以确保数据的正确性和一致性。接下来我们一起探讨MyBatis
中TypeHandler
的相关知识,包括其基础原理、编写自定义TypeHandler
、处理常见数据类型、高级应用以及性能优化等内容。
TypeHandler的工作原理
TypeHandler 的工作原理主要体现在两个关键环节:参数设置和结果集映射。
- 参数设置 : 当
MyBatis
执行SQL
语句时,需要将用户传入的方法参数或者Mapper XML
文件中定义的参数值设置到PreparedStatement
对象中。对于非基本类型的参数,如自定义对象、枚举或其他复杂类型,MyBatis
将通过查找对应的TypeHandler
实现类来完成转换工作。即MyBatis
根据参数的Java
类型找到对应的TypeHandler
,然后调用其setParameter
方法,这个方法会将Java
类型的数据转换为JDBC
可识别的数据库类型,并调用PreparedStatement
的set
方法将转换后的数据写入预编译的SQL
语句中。
image.png
- 结果集映射 : 在查询执行完毕后,
MyBatis
需要将从ResultSet
中读取的数据转换成Java
类型并填充到目标对象属性上。如下即为根据jdbcType
或者javaType
获取对对应的typeHandler
。
image.png
如果在Mybatis的xml中指定了TypeHandler则会直接使用这个Handler。
MyBatis
通过结果映射配置来确定将结果集中的哪些列映射到Java
对象的哪些属性上。当MyBatis
从ResultSet
中获取某列数据时,它会根据结果映射配置所关联的Java
类型,找到相应的TypeHandler
。然后,MyBatis
调用TypeHandler
的getResult
方法,该方法将从数据库返回的JDBC
类型数据转换为Java
类型,并最终赋值给目标Java
对象的属性。这个过程确保了数据库中的数据与Java
对象之间的正确转换和映射,从而实现了数据的持久化操作。
image.png
自定义TypeHandler
在MyBatis
中,虽然已经提供了丰富的内置TypeHandler
来处理常见的数据类型,但在实际开发中,有时候我们可能需要处理一些特殊的数据类型或者定制化的数据转换逻辑,例如数据库中的某个字段存储的是特定格式的字符串(例如JSON数据类型),但Java
端需要将其转换为枚举或自定义对象。这时候,就需要编写自定义的TypeHandler
来进行数据处理。
TypeHandler接口
要编写自定义的TypeHandler
,首先需要实现MyBatis
提供的TypeHandler
接口。该接口定义了处理结果集方法getResult
和处理参数的方法setParameter
。
image.png
在实现TypeHandler
接口后,我们需要重写这几个方法,setParameter
方法用于将Java
对象的属性值设置到PreparedStatement
中,而getResult
方法则用于将ResultSet
中的数据转换为Java
对象的属性值。
BaseTypeHandler抽象类
实际上,我们在日常开发中常使用的并不是实现TypeHandler
接口,而是继承BaseTypeHandler
抽象类。BaseTypeHandler
实现了TypeHandler
接口,BaseTypeHandler
提供了对TypeHandler
接口中方法的默认实现,包括空值处理、异常处理等,减少了重复的代码编写,提高了开发效率。同时,BaseTypeHandler
也提供了一些扩展点,使得开发者可以在不改变核心逻辑的情况下进行定制化扩展,满足不同场景下的需求。使用BaseTypeHandler
抽象类可以使自定义TypeHandler
的编写更加规范、简化和灵活。
image.png
MappedJdbcTypes和MappedTypes
在自定义的TypeHanlder
时,我们也可以使用@MappedJdbcTypes
和@MappedTypes
注解,显示的指定你的TypeHanlder
要处理的JDBC
类型和Java
类型。
@MappedJdbcTypes
注解用于指定该TypeHandler支持的JDBC类型。通过这个注解,可以明确告知Mybatis
此类型处理器在getResult
时应该处理哪些数据库中的数据类型(如VARCHAR
、TIMESTAMP
等)。
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
时,通过SqlSessionFactoryBean
的setTypeHandlers
的方法全局指定你的TypeHandler
。
ini
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeHandlers(new AddressTypeHandler());
此时我们就可以在TypeHandlerMap
中可以发现这个自定义的TypeHanlder
了。
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">
<!--@mbg.generated-->
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
来进行数据转换。
示例
我们以Mysql
的JSON
数据类型为例,以《解锁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();
}
这样一个通用的JSON
转Java
对象的通用Handler
就完成了。然后我们具体的字段转换到相应的Java
对象时,只需要继承这个抽象类,把Java
对象传递过去即可。
csharp
public class AddressTypeHandler extends JsonBaseTypeHandler<AddressBO> {
/**
* 具体类型,由子类提供
*
* @return 具体类型
*/
@Override
protected TypeReference<AddressBO> specificType() {
return new TypeReference<AddressBO>() {};
}
}
然后我们分别指定查询的ResultMap
以及插入的sql
中的address
字段的TypeHandler
为AddressTypeHandler
的全路径。
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">
<!--@mbg.generated-->
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
然后我们在执行查询方法:
csharp
@Test
public void listUserTest(){
UserInfoDO userInfoDO = userInfoMapper.selectByPrimaryKey(6L);
System.out.println(userInfoDO);
}
打印结果为:
image.png
在MyBatis
框架中,采用自定义TypeHandler
实现JSON
数据类型字段与Java
对象的相互转换具有显著的优势。通过精心设计和实现TypeHandler
,可以精准把控从Java
对象到JSON
字符串以及反向转换的过程,确保数据在存入数据库时按照预设格式可靠地序列化,并在读取时准确无误地还原为对应的Java
实体,从而有效避免因数据格式不兼容引发的运行时异常或数据损坏问题。同时,利用TypeHandler
将数据持久化的具体逻辑进行抽象封装,使业务代码得以聚焦核心功能,不受底层数据库交互细节的影响,极大提升了代码的可读性和维护性。而在整个项目范围内统一应用自定义的TypeHandler
,有利于维持数据操作的一致性和标准化,消除了由于开发人员使用不同处理策略带来的潜在风险,有力推动了项目的整体开发效率和维护质量提升。
总结
本文介绍了MyBatis
中TypeHandler
的概念、基础用法以及应用。我们探讨了如何编写自定义的 TypeHandler
来处理特殊格式的数据。通过本文的学习,我们可以更好地理解和应用TypeHandler
,在实际开发中处理数据库操作时能够更加灵活、高效地运用MyBatis
框架。
在使用TypeHandler
的过程中,我们需要注意数据的准确性和一致性,确保数据的正确转换和映射,避免出现数据丢失或者转换错误的情况。除了简单的数据类型转换外,TypeHandler
还可以用于进行数据校验和转换。在TypeHandler
的实现中,我们可以添加一些逻辑来对数据进行校验,如检查数据的有效性、范围等。通过在TypeHandler
中添加数据校验和转换逻辑,我们可以确保数据的完整性和正确性。
另外,还需要根据具体的业务需求和数据特点来选择合适的 TypeHandler
,并根据需要进行性能优化和调整,以提高系统的整体性能和稳定性。
通过对TypeHandler
的学习和实践,我们可以更加灵活地处理各种数据类型和数据格式,为项目的顺利进行和未来的发展打下坚实的基础。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等