在使用 MyBatis-Plus 进行数据库操作时,经常会遇到数据库字段类型与 Java 实体类属性类型不一致的问题。例如,数据库中存储的是 JSON 字符串,而 Java 实体类需要的是一个 List 或 Map。这时,MyBatis-Plus 提供的字段类型处理器(TypeHandler)就能派上用场,它可以在 MyBatis 自动映射结果集的过程中,实现类型转换,避免手动编写大量的 XML 配置。
常见的字段类型转换场景
- JSON 字符串与 List/Map 的互转:数据库字段存储 JSON 格式的数据,Java 实体类属性定义为 List 或 Map 类型。
- 枚举类型与数据库字符串/数字的互转:数据库字段存储枚举类型的字符串或数字值,Java 实体类属性定义为枚举类型。
- 日期类型处理:处理数据库中时间戳、日期字符串与 Java Date/LocalDateTime 等类型的转换。
MyBatis-Plus TypeHandler 底层原理
MyBatis-Plus 的 TypeHandler 实际上是对 MyBatis 原生 TypeHandler 的扩展和封装。MyBatis 在执行 SQL 查询后,会通过 ResultSetExtractor 将结果集映射到 Java 对象。在这个映射过程中,如果遇到需要类型转换的字段,就会调用相应的 TypeHandler 进行处理。
TypeHandler 的核心方法包括:
setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType):将 Java 类型的参数转换为 JDBC 类型,用于 SQL 语句的参数绑定。getResult(ResultSet rs, String columnName):从 ResultSet 中获取指定列的值,并转换为 Java 类型。getResult(ResultSet rs, int columnIndex):从 ResultSet 中获取指定索引的列的值,并转换为 Java 类型。getResult(CallableStatement cs, int columnIndex):从 CallableStatement 中获取指定索引的参数值,并转换为 Java 类型。(存储过程)
MyBatis-Plus 已经内置了一些常用的 TypeHandler,例如 JacksonTypeHandler 用于 JSON 类型的转换。如果没有满足需求的 TypeHandler,可以自定义 TypeHandler 来实现更复杂的类型转换逻辑。
自定义 MyBatis-Plus TypeHandler 实战
场景:数据库字段 config 存储 JSON 格式的配置信息,Java 实体类 User 的 config 属性定义为 Map<String, Object> 类型。
步骤:
- 创建自定义 TypeHandler:
import com.baomidou.mybatisplus.core.handlers.AbstractTypeHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class JsonMapTypeHandler extends AbstractTypeHandler<Map<String, Object>> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
try {
ps.setString(i, objectMapper.writeValueAsString(parameter)); // 将 Map 转换为 JSON 字符串
} catch (Exception e) {
throw new SQLException(e);
}
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJsonToMap(json); // 将 JSON 字符串转换为 Map
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJsonToMap(json);
}
@Override
public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJsonToMap(json);
}
private Map<String, Object> parseJsonToMap(String json) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return objectMapper.readValue(json, Map.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 在实体类中指定 TypeHandler:
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Map;
@Data
@TableName("user")
public class User {
private Long id;
private String name;
@TableField(value = "config", typeHandler = JsonMapTypeHandler.class) // 使用自定义 TypeHandler
private Map<String, Object> config;
}
- 在 MyBatis-Plus 配置中注册 TypeHandler(可选,如果使用全局类型处理器,则需要注册。也可以不注册,仅在
@TableField中指定):
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// ... 其他拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.getTypeHandlerRegistry().register(Map.class, JdbcType.VARCHAR, new JsonMapTypeHandler()); // 全局注册
};
}
}
实战避坑经验总结
- 空指针异常:确保 TypeHandler 中处理 null 值的情况,避免空指针异常。可以使用
getNullableResult方法处理。 - JSON 解析异常:确保数据库中存储的 JSON 字符串格式正确,否则会导致 JSON 解析异常。可以使用 try-catch 块捕获异常并记录日志。
- 性能问题:避免在 TypeHandler 中执行复杂的逻辑,影响性能。可以将复杂的逻辑放在业务层处理。
- 字符集问题:确保数据库连接字符集与 Java 项目字符集一致,避免中文乱码问题。
- 升级风险:MyBatis 和 MyBatis-Plus 版本升级时,TypeHandler 的 API 可能会发生变化,需要及时更新代码。
结合 Nginx、Redis 和 MySQL 的高并发场景考虑
在高并发场景下,例如使用 Nginx 作为反向代理服务器,通过负载均衡将请求分发到多个应用服务器,并使用 Redis 作为缓存,MySQL 存储持久化数据。此时,TypeHandler 的性能就显得尤为重要。需要尽量减少 TypeHandler 的执行时间,避免阻塞请求处理线程。例如,可以考虑使用更高效的 JSON 库,或者将 JSON 解析结果缓存到 Redis 中,减少数据库的访问压力。同时,也要注意 MySQL 的连接池配置,例如最大连接数、最小空闲连接数等,避免连接池耗尽导致服务不可用。可以使用 Druid 这样的数据库连接池监控工具,实时监控数据库连接池的状态。
对于使用了宝塔面板的用户,可以方便地通过宝塔面板管理 Nginx 和 MySQL,但也要注意宝塔面板默认的配置可能不是最优的,需要根据实际情况进行调整,例如调整 Nginx 的并发连接数、MySQL 的 buffer pool 大小等。
冠军资讯
脱发程序员