💡 前言
在实际开发中,我们经常需要将 Java 中的复杂对象(如 List<String>、自定义类等)与数据库字段进行映射。然而,MyBatis 并不能直接识别这些非标准类型,这就需要用到它的扩展机制 —— BaseTypeHandler。
本文将以 List<String> 为例,详细讲解如何通过继承 BaseTypeHandler 实现 Java 类型与数据库字符串之间的双向转换,并结合 Spring Boot + MyBatis 给出完整示例,帮助你在项目中灵活处理各种复杂数据类型的持久化问题。
🛠️ 一、为什么需要 BaseTypeHandler?
默认情况下,MyBatis 支持常见的 Java 类型与 JDBC 类型之间的自动映射,例如:
| Java 类型 | JDBC 类型 |
|---|---|
| String | VARCHAR |
| Integer | INTEGER |
| Date | DATE |
但如果你有一个自定义对象或集合类型(如 List<String>),就需要自己实现一个类型处理器来告诉 MyBatis 如何将这个类型写入数据库,以及如何从结果集中读取回来。
这就是 BaseTypeHandler 的作用!
📦 二、环境准备
1. 添加依赖(pom.xml)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 数据库配置(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.model
🧪 三、实战:使用 BaseTypeHandler 处理 List
假设我们要存储一个商品(Product)的信息,其中包含一组标签(tags),以逗号分隔的形式保存在数据库中。但在 Java 层面,我们希望它是一个 List<String> 类型。
1. 定义实体类(Product.java)
@Data
public class Product {
private Long id;
private String name;
private List<String> tags; // 我们要处理的 List<String>
}
2. 创建 BaseTypeHandler 实现类(StringToListTypeHandler.java)
/**
* @author : pzj
* @FILENAME: TypeHandler
* @DATE: 2023/8/4
* @Direction: Json数据转换
*/
@MappedJdbcTypes(JdbcType.VARCHAR) // 数据库中该字段存储的类型
@MappedTypes(List.class) // 需要转换的对象
public class ListStringTypeHandler extends BaseTypeHandler<List<String>> {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
ps.setObject(i, JSON.toJSONString(parameter));
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return getString(rs.getString(columnName));
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return getString(rs.getString(columnIndex));
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return getString(cs.getString(columnIndex));
}
private List<String> getString(String value) {
if (StringUtils.hasText(value)) {
try {
CollectionType type = objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, String.class);
return objectMapper.readValue(value, type);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return null;
}
}
3. Mapper 接口(ProductMapper.java)
@Mapper
public interface ProductMapper {
public Product selectById(Long id);
public int insert(Product product);
}
4. Mapper XML 文件(ProductMapper.xml)
<mapper namespace="com.example.demo.mapper.ProductMapper">
<resultMap id="ProductResultMap" type="Product">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="tags" property="tags"
typeHandler="com.example.demo.handler.StringToListTypeHandler"/>
</resultMap>
<insert id="insert">
INSERT INTO product(name, tags)
VALUES (
#{name},
#{tags, typeHandler=com.example.demo.handler.StringToListTypeHandler}
)
</insert>
<select id="selectById" resultMap="ProductResultMap">
SELECT * FROM product WHERE id = #{id}
</select>
</mapper>
5. 数据表结构(product 表)
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
tags TEXT -- 存储格式:"tag1,tag2,tag3"
);
🧭 四、测试代码(TestController.java)
@RestController
@RequestMapping("/products")
public class TestController {
@Autowired
private ProductMapper productMapper;
@PostMapping
public void createProduct(@RequestBody Product product) {
productMapper.insert(product);
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productMapper.selectById(id);
}
}
示例请求:
插入数据:
{
"name": "笔记本",
"tags": ["电子", "数码", "办公"]
}
查询返回:
{
"id": 1,
"name": "笔记本",
"tags": ["电子", "数码", "办公"]
}
🧠 五、进阶技巧
✅ 使用 JSON 序列化更复杂的对象
如果字段是 List<User> 或其他复杂对象,可以考虑用 Jackson 或 Gson 序列化成 JSON 字符串存入数据库:
// 存入时
ObjectMapper mapper = new ObjectMapper();
ps.setString(i, mapper.writeValueAsString(parameter));
// 读取时
return mapper.readValue(rs.getString(columnName), new TypeReference<List<User>>() {});
⚠️ 六、注意事项
| 问题 | 解决方案 |
|---|---|
Column 'tags' cannot be null | 确保插入时 tags 不为空,可用 Collections.emptyList() 替代 null |
No typehandler found | 检查是否正确配置了 typeHandler 或是否漏写了包路径 |
SQLSyntaxErrorException | 检查数据库字段是否为 TEXT 或 VARCHAR 类型 |
ConcurrentModificationException | 如果 List 是只读的,请确保返回的是新的可变列表 |
📘 七、总结
| 功能 | 描述 |
|---|---|
| BaseTypeHandler | 自定义 Java 类型和数据库类型的映射关系 |
| 场景举例 | List ↔ CSV 字符串 |
| 核心方法 | setNonNullParameter(写入)、getNullableResult(读取) |
| 扩展建议 | 可用于枚举、JSON 对象、自定义类等复杂类型转换 |
掌握 BaseTypeHandler 的使用,不仅能让你更灵活地处理各种数据结构,还能提升系统的可维护性和扩展性。无论是电商系统、内容管理平台还是后台管理系统,都能从中受益!
如果你觉得有用,欢迎点赞、收藏、转发给更多需要的朋友!