介绍
在 Java 开发中,TypeHandler 是 MyBatis 框架中的一个核心组件,用于实现数据库与 Java 类型之间的相互转换。它允许开发人员自定义类型处理器,以满足特定的业务需求。
TypeHandler 的作用是在 MyBatis 执行 SQL 查询或更新操作时,将数据库中的列值转换为 Java 对象,并在将 Java 对象写入数据库时执行相反的转换。它提供了一种灵活且可扩展的方式,用于处理数据库类型与 Java 类型之间的映射关系。
关键特点和使用方式:
- 类型转换:TypeHandler 负责将数据库中的列值转换为 Java 对象,并将 Java 对象转换为数据库可接受的类型。
- 自定义类型处理器:MyBatis 提供了一些默认的类型处理器,例如处理整数、字符串、日期等常见类型。同时,开发人员可以根据需要自定义类型处理器,实现特定类型的转换逻辑。
- 注册类型处理器:自定义的类型处理器需要在 MyBatis 的配置文件中进行注册,以便 MyBatis 在执行数据库操作时能够找到并使用它们。
- 支持复杂类型:TypeHandler 不仅可以处理基本类型,还可以处理复杂类型,例如枚举、自定义对象等。
- 映射规则:TypeHandler 的使用是基于映射规则的,即在 MyBatis 的映射文件中,通过指定列和属性之间的映射关系,TypeHandler 才能正确地进行类型转换。
源码
1. BaseTypeHandler 源码
类型处理器的基类 BaseTypeHandler
/**
* @author Clinton Begin
* @author Simone Tripodi
*/
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
protected Configuration configuration;
public void setConfiguration(Configuration c) {
// 成员属性的set方法中,用到了this关键字
this.configuration = c;
}
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
// 参数值为null,又没有设置jdbcType,MyBatis会手动抛出TypeException异常
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
// 手动抛出自定义异常TypeException【哈哈,项目中常遇到的异常】
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
"Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
"Try setting a different JdbcType for this parameter or a different configuration property. " +
"Cause: " + e, e);
}
}
}
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
T result;
try {
result = getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
if (rs.wasNull()) {
return null;
} else {
return result;
}
}
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
T result;
try {
result = getNullableResult(rs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from result set. Cause: " + e, e);
}
if (rs.wasNull()) {
return null;
} else {
return result;
}
}
@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
T result;
try {
result = getNullableResult(cs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from callable statement. Cause: " + e, e);
}
if (cs.wasNull()) {
return null;
} else {
return result;
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
}
2. JdbcType源码
/**
* @author Clinton Begin
*/
public enum JdbcType {
/*
* This is added to enable basic support for the
* ARRAY data type - but a custom type handler is still required
*/
ARRAY(Types.ARRAY),
BIT(Types.BIT),
TINYINT(Types.TINYINT),
SMALLINT(Types.SMALLINT),
INTEGER(Types.INTEGER),
BIGINT(Types.BIGINT),
FLOAT(Types.FLOAT),
REAL(Types.REAL),
DOUBLE(Types.DOUBLE),
NUMERIC(Types.NUMERIC),
DECIMAL(Types.DECIMAL),
CHAR(Types.CHAR),
VARCHAR(Types.VARCHAR),
LONGVARCHAR(Types.LONGVARCHAR),
DATE(Types.DATE),
TIME(Types.TIME),
TIMESTAMP(Types.TIMESTAMP),
BINARY(Types.BINARY),
VARBINARY(Types.VARBINARY),
LONGVARBINARY(Types.LONGVARBINARY),
NULL(Types.NULL),
OTHER(Types.OTHER),
BLOB(Types.BLOB),
CLOB(Types.CLOB),
BOOLEAN(Types.BOOLEAN),
CURSOR(-10), // Oracle
UNDEFINED(Integer.MIN_VALUE + 1000),
NVARCHAR(Types.NVARCHAR), // JDK6
NCHAR(Types.NCHAR), // JDK6
NCLOB(Types.NCLOB), // JDK6
STRUCT(Types.STRUCT),
JAVA_OBJECT(Types.JAVA_OBJECT),
DISTINCT(Types.DISTINCT),
REF(Types.REF),
DATALINK(Types.DATALINK),
ROWID(Types.ROWID), // JDK6
LONGNVARCHAR(Types.LONGNVARCHAR), // JDK6
SQLXML(Types.SQLXML), // JDK6
DATETIMEOFFSET(-155); // SQL Server 2008
public final int TYPE_CODE;
private static Map<Integer,JdbcType> codeLookup = new HashMap<Integer,JdbcType>();
static {
for (JdbcType type : JdbcType.values()) {
codeLookup.put(type.TYPE_CODE, type);
}
}
JdbcType(int code) {
this.TYPE_CODE = code;
}
public static JdbcType forCode(int code) {
return codeLookup.get(code);
}
}
案例1 springboot+mybatis
1. 创建自定义 TypeHandler:继承 BaseTypeHandler 并实现方法
枚举类型的处理
假设我们有一个订单实体类 Order,其中包含一个 Status 枚举类型的状态字段,在数据库中,我们将状态字段存储为一个字符串类型的列,例如 status。
在下述示例中,我们自定义了一个 TypeHandler 类 StatusTypeHandler,用于处理 Status 枚举类型与数据库列值之间的转换。我们需要继承 BaseTypeHandler 并重写相应的方法来实现转换逻辑。
package org.zyf.javabasic.mybatis;
/**
* @author yanfengzhang
* @description 订单实体类 Order
* 其中包含一个 Status 枚举类型
* @date 2023/5/13 23:34
*/
public enum Status {
CREATED,
PROCESSING,
COMPLETED,
CANCELLED
}
//我们可以使用自定义的 TypeHandler 来实现 Status 枚举类型与数据库列值之间的转换。
//以下是一个示例的 TypeHandler 实现:
package org.zyf.javabasic.mybatis;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author yanfengzhang
* @description 自定义的 TypeHandler 来实现 Status 枚举类型与数据库列值之间的转换
* @date 2023/5/13 23:36
*/
public class StatusTypeHandler extends BaseTypeHandler<Status> {
@Override
public Status getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 从 ResultSet 中获取列值
int data = rs.getInt(columnName);
// 将列值转换为枚举对象
return Status.getEnumById(data);
}
@Override
public Status getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int data = rs.getInt(columnIndex);
return Status.getEnumById(data);
}
@Override
public Status getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
int data = callableStatement.getInt(columnIndex);
return Status.getEnumById(data);
}
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Status status, JdbcType jdbcType) throws SQLException {
// 将枚举值的code存储到数据库中
preparedStatement.setInt(i, status.getCode());
}
}
2. 注册 TypeHandler
然后在 application.yml 或 application.properties 中启用自动配置,并指定 MyBatis 自动扫描自定义的 TypeHandler。
在 application.yml 中配置:
mybatis:
type-handlers-package: com.example.demo.typehandler
在 application.properties 中配置:
mybatis.type-handlers-package=com.example.demo.typehandler
这一步会告诉 MyBatis 扫描 com.example.demo.typehandler 包中的所有自定义 TypeHandler。
3. 在映射文件(XML或注解)中使用 TypeHandler
接下来,在 MyBatis 的映射文件中,你可以直接使用你定义的 TypeHandler。假设你在数据库中存储的是一个 String,但是你希望映射成一个自定义的类型 MyCustomType。
XML 映射文件:
<resultMap id="MyResultMap" type="com.example.demo.MyEntity">
<result column="my_column" property="myCustomField" typeHandler="com.example.demo.typehandler.MyCustomTypeHandler"/>
</resultMap>
注解方式:
如果你使用注解而不是 XML 配置,可以通过 @Result 注解的 typeHandler 属性来指定:
@Mapper
public interface MyMapper {
@Select("SELECT my_column FROM my_table WHERE id = #{id}")
@Result(column = "my_column", property = "myCustomField", typeHandler = MyCustomTypeHandler.class)
MyEntity findById(int id);
}
案例2 springboot+mybatisPlus
1. 创建自定义的TypeHandler
首先,你需要创建一个实现了org.apache.ibatis.type.TypeHandler接口的类。
假设你要处理一个Integer到String的自定义转换,下面是一个简单的例子:
package com.example.demo.handler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class IntegerToStringTypeHandler extends BaseTypeHandler<Integer> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException {
// 将Integer转换成String并设置到PreparedStatement中
ps.setString(i, parameter != null ? parameter.toString() : null);
}
@Override
public Integer getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 从ResultSet中获取String类型的数据并转换成Integer
String result = rs.getString(columnName);
return result != null ? Integer.valueOf(result) : null;
}
@Override
public Integer getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// 从ResultSet中获取String类型的数据并转换成Integer
String result = rs.getString(columnIndex);
return result != null ? Integer.valueOf(result) : null;
}
@Override
public Integer getNullableResult(java.sql.CallableStatement cs, int columnIndex) throws SQLException {
// 从CallableStatement中获取数据并进行转换
String result = cs.getString(columnIndex);
return result != null ? Integer.valueOf(result) : null;
}
}
2. 注册TypeHandler
MyBatis允许你在配置中注册自定义的TypeHandler。你可以通过在application.yml或application.properties中进行配置,或者通过Java配置来实现。
方法一:通过application.yml进行配置
在application.yml中注册自定义的TypeHandler:
mybatis-plus:
configuration:
typeHandlersPackage: com.example.demo.handler
方法二:通过Java配置注册
你也可以通过在@Configuration类中进行注册:
package com.example.demo.config;
import com.example.demo.handler.IntegerToStringTypeHandler;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
// 设定数据源
sessionFactoryBean.setDataSource(dataSource());
// 注册自定义的TypeHandler
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.getTypeHandlerRegistry().register(IntegerToStringTypeHandler.class);
sessionFactoryBean.setConfiguration(configuration);
return sessionFactoryBean.getObject();
}
private javax.sql.DataSource dataSource() {
// 你可以返回你自己的数据源实例
return null;
}
}
3. 使用自定义的TypeHandler
在实体类的字段上使用自定义的TypeHandler。你可以通过@TableField注解的typeHandler属性指定:
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.demo.handler.IntegerToStringTypeHandler;
@TableName("user")
public class User {
private Long id;
@TableField(typeHandler = IntegerToStringTypeHandler.class)
private Integer age;
// getters and setters
}
4. 运行应用
当你启动应用时,MyBatis-Plus会自动加载配置并使用你自定义的TypeHandler来转换数据类型。