Spring Boot 整合 MyBatis 中使用 BaseTypeHandler 处理复杂类型详解

371 阅读3分钟

💡 前言

在实际开发中,我们经常需要将 Java 中的复杂对象(如 List<String>、自定义类等)与数据库字段进行映射。然而,MyBatis 并不能直接识别这些非标准类型,这就需要用到它的扩展机制 —— BaseTypeHandler

本文将以 List<String> 为例,详细讲解如何通过继承 BaseTypeHandler 实现 Java 类型与数据库字符串之间的双向转换,并结合 Spring Boot + MyBatis 给出完整示例,帮助你在项目中灵活处理各种复杂数据类型的持久化问题。


🛠️ 一、为什么需要 BaseTypeHandler?

默认情况下,MyBatis 支持常见的 Java 类型与 JDBC 类型之间的自动映射,例如:

Java 类型JDBC 类型
StringVARCHAR
IntegerINTEGER
DateDATE

但如果你有一个自定义对象或集合类型(如 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检查数据库字段是否为 TEXTVARCHAR 类型
ConcurrentModificationException如果 List 是只读的,请确保返回的是新的可变列表

📘 七、总结

功能描述
BaseTypeHandler自定义 Java 类型和数据库类型的映射关系
场景举例List ↔ CSV 字符串
核心方法setNonNullParameter(写入)、getNullableResult(读取)
扩展建议可用于枚举、JSON 对象、自定义类等复杂类型转换

掌握 BaseTypeHandler 的使用,不仅能让你更灵活地处理各种数据结构,还能提升系统的可维护性和扩展性。无论是电商系统、内容管理平台还是后台管理系统,都能从中受益!

如果你觉得有用,欢迎点赞、收藏、转发给更多需要的朋友!