JAVA MyBatis 无法识别枚举?TypeHandler 注册方式讲解

MyBatis 与枚举类型:问题、解决方案与最佳实践

大家好,今天我们来聊聊 MyBatis 在处理 Java 枚举类型时可能遇到的问题,以及如何通过自定义 TypeHandler 来优雅地解决这些问题,并深入探讨 MyBatis 中注册 TypeHandler 的各种方式。

枚举类型与 MyBatis 的默认行为

默认情况下,MyBatis 对 Java 枚举类型的处理方式可能会出乎一些开发者的意料。简单来说,MyBatis 会尝试将枚举对象映射到数据库的某个列上,通常情况下,这个列的数据类型会是字符串或者整数。

假设我们有一个简单的枚举类型 OrderStatus

public enum OrderStatus {
    CREATED("创建"),
    PAID("已支付"),
    SHIPPED("已发货"),
    COMPLETED("已完成"),
    CANCELLED("已取消");

    private final String description;

    OrderStatus(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

如果我们直接在 MyBatis 的 Mapper 文件中使用这个枚举类型,例如:

<insert id="insertOrder">
    INSERT INTO orders (order_id, status) VALUES (#{orderId}, #{status})
</insert>

<select id="getOrderById" resultType="Order">
    SELECT order_id, status FROM orders WHERE order_id = #{orderId}
</select>

其中,Order 类中包含一个 OrderStatus 类型的属性 status

public class Order {
    private Long orderId;
    private OrderStatus status;

    // Getters and Setters
}

如果数据库中 status 列的类型是 VARCHAR,MyBatis 会尝试将 OrderStatus 枚举对象的 toString() 方法的返回值(即枚举常量的名称,例如 "CREATED")存储到数据库中。 读取的时候也会将数据库中的字符串匹配到对应的枚举常量。 这种方式在很多情况下都能工作,但存在一些问题:

  • 代码可读性降低: 数据库中存储的是枚举常量的名称,而不是更友好的描述信息。
  • 数据库维护性降低: 如果枚举常量的名称发生变化,数据库中的数据也需要进行更新。
  • 类型安全性降低: 数据库中存储的字符串可以是任意值,而不仅仅是枚举常量的值,这可能会导致数据不一致。

如果 status 列的类型是 INT,MyBatis 会尝试将 OrderStatus 枚举对象的 ordinal() 方法的返回值(即枚举常量在枚举类中定义的顺序,从 0 开始)存储到数据库中。 读取的时候也会将整数匹配到对应的枚举常量。 这种方式存在更严重的问题:

  • 代码可读性极差: 数据库中存储的是数字,完全无法理解其含义。
  • 数据库维护性极差: 如果枚举常量的顺序发生变化,数据库中的数据将变得完全错误。
  • 类型安全性极差: 数据库中存储的数字可以是任意值,而不仅仅是枚举常量的值,这可能会导致数据不一致。

因此,我们需要自定义 TypeHandler 来解决这些问题。

自定义 TypeHandler

TypeHandler 是 MyBatis 中用于处理 Java 类型和 JDBC 类型之间转换的接口。 通过自定义 TypeHandler,我们可以控制枚举类型如何被写入数据库以及如何从数据库读取。

1. 基于字符串的 TypeHandler

如果我们希望将枚举的描述信息存储到数据库中,可以创建一个基于字符串的 TypeHandler:

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;

public class OrderStatusTypeHandler extends BaseTypeHandler<OrderStatus> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.getDescription());
    }

    @Override
    public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String statusDescription = rs.getString(columnName);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    @Override
    public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String statusDescription = rs.getString(columnIndex);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    @Override
    public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String statusDescription = cs.getString(columnIndex);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    private OrderStatus getOrderStatusByDescription(String description) {
        for (OrderStatus status : OrderStatus.values()) {
            if (status.getDescription().equals(description)) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid OrderStatus description: " + description);
    }
}

这个 TypeHandler 继承自 BaseTypeHandler<OrderStatus>,并重写了四个方法:

  • setNonNullParameter():将枚举对象的描述信息设置到 PreparedStatement 中。
  • getNullableResult():从 ResultSet 或 CallableStatement 中获取描述信息,并将其转换为枚举对象。
  • getOrderStatusByDescription(): 根据描述信息查找对应的枚举类型,如果找不到,抛出异常。

2. 基于整数的 TypeHandler

如果我们希望将枚举的某种数字编码存储到数据库中,例如使用自定义的状态码,可以创建一个基于整数的 TypeHandler。 首先,需要在枚举类中定义状态码:

public enum OrderStatus {
    CREATED(1, "创建"),
    PAID(2, "已支付"),
    SHIPPED(3, "已发货"),
    COMPLETED(4, "已完成"),
    CANCELLED(5, "已取消");

    private final int code;
    private final String description;

    OrderStatus(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    public static OrderStatus fromCode(int code) {
        for (OrderStatus status : OrderStatus.values()) {
            if (status.getCode() == code) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid OrderStatus code: " + code);
    }
}

然后,创建 TypeHandler:

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;

public class OrderStatusCodeTypeHandler extends BaseTypeHandler<OrderStatus> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int statusCode = rs.getInt(columnName);
        if (rs.wasNull()) {
            return null;
        }
        return OrderStatus.fromCode(statusCode);
    }

    @Override
    public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int statusCode = rs.getInt(columnIndex);
        if (rs.wasNull()) {
            return null;
        }
        return OrderStatus.fromCode(statusCode);
    }

    @Override
    public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int statusCode = cs.getInt(columnIndex);
        if (cs.wasNull()) {
            return null;
        }
        return OrderStatus.fromCode(statusCode);
    }
}

这个 TypeHandler 与基于字符串的 TypeHandler 类似,只是将字符串操作替换为了整数操作。 需要注意的是,在 getNullableResult 方法中,需要使用 rs.wasNull() 方法来判断数据库中的值是否为 NULL。

注册 TypeHandler

自定义 TypeHandler 之后,需要将其注册到 MyBatis 中才能生效。 MyBatis 提供了多种注册 TypeHandler 的方式。

1. 在 MyBatis 配置文件中注册

可以在 MyBatis 的配置文件 mybatis-config.xml 中注册 TypeHandler:

<configuration>
    <typeHandlers>
        <typeHandler handler="com.example.OrderStatusTypeHandler" javaType="com.example.OrderStatus"/>
        <typeHandler handler="com.example.OrderStatusCodeTypeHandler" javaType="com.example.OrderStatus"/>
    </typeHandlers>
</configuration>
  • handler 属性指定 TypeHandler 类的全限定名。
  • javaType 属性指定该 TypeHandler 处理的 Java 类型。

这种方式的优点是配置集中,易于管理。 缺点是需要修改 MyBatis 配置文件,不够灵活。

2. 在 Mapper XML 文件中注册

可以在 Mapper XML 文件中为特定的属性注册 TypeHandler:

<insert id="insertOrder" parameterType="Order">
    INSERT INTO orders (order_id, status)
    VALUES (#{orderId}, #{status, typeHandler=com.example.OrderStatusTypeHandler})
</insert>

<select id="getOrderById" resultType="Order">
    SELECT order_id, status
    FROM orders
    WHERE order_id = #{orderId}
</select>

<resultMap id="orderResultMap" type="Order">
    <id property="orderId" column="order_id"/>
    <result property="status" column="status" typeHandler="com.example.OrderStatusTypeHandler"/>
</resultMap>
  • <insert> 语句中,可以使用 typeHandler 属性为 status 属性指定 TypeHandler。
  • <resultMap> 中,可以使用 typeHandler 属性为 status 属性指定 TypeHandler。

这种方式的优点是灵活,可以为不同的属性使用不同的 TypeHandler。 缺点是配置分散,不易于管理。

3. 使用注解注册

可以使用 @MappedTypes@MappedJdbcTypes 注解来注册 TypeHandler。 首先,需要创建一个类来实现 TypeHandler 接口,而不是继承 BaseTypeHandler

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedTypes(OrderStatus.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class OrderStatusAnnotationTypeHandler implements TypeHandler<OrderStatus> {

    @Override
    public void setParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.getDescription());
    }

    @Override
    public OrderStatus getResult(ResultSet rs, String columnName) throws SQLException {
        String statusDescription = rs.getString(columnName);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    @Override
    public OrderStatus getResult(ResultSet rs, int columnIndex) throws SQLException {
        String statusDescription = rs.getString(columnIndex);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    @Override
    public OrderStatus getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String statusDescription = cs.getString(columnIndex);
        if (statusDescription == null) {
            return null;
        }
        return getOrderStatusByDescription(statusDescription);
    }

    private OrderStatus getOrderStatusByDescription(String description) {
        for (OrderStatus status : OrderStatus.values()) {
            if (status.getDescription().equals(description)) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid OrderStatus description: " + description);
    }
}
  • @MappedTypes(OrderStatus.class) 注解指定该 TypeHandler 处理的 Java 类型。
  • @MappedJdbcTypes(JdbcType.VARCHAR) 注解指定该 TypeHandler 处理的 JDBC 类型。

然后,需要在 MyBatis 配置文件中扫描该 TypeHandler 所在的包:

<configuration>
    <typeHandlers>
        <package name="com.example"/>
    </typeHandlers>
</configuration>

这种方式的优点是简洁,不需要在 Mapper XML 文件中指定 TypeHandler。 缺点是需要修改 MyBatis 配置文件,并且需要扫描包,可能会影响性能。

4. 全局默认 TypeHandler

MyBatis 允许为某种 Java 类型设置全局默认的 TypeHandler。 这样,所有该类型的属性都会使用该 TypeHandler,而不需要在 Mapper XML 文件中显式指定。

<configuration>
    <typeHandlers>
        <typeHandler handler="com.example.OrderStatusTypeHandler" javaType="com.example.OrderStatus" default="true"/>
    </typeHandlers>
</configuration>
  • default="true" 属性表示该 TypeHandler 是该 Java 类型的全局默认 TypeHandler。

这种方式的优点是简单,可以避免在 Mapper XML 文件中重复指定 TypeHandler。 缺点是缺乏灵活性,所有该类型的属性都会使用同一个 TypeHandler。

5. 使用 MyBatis-Spring 集成

如果使用 MyBatis-Spring 集成,可以通过 Spring 的配置来注册 TypeHandler。

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class MyBatisConfig {

    // 这里不需要显式配置 TypeHandler,MyBatis-Spring 会自动扫描并注册 TypeHandler
}

同时,在 Spring 的配置文件中,需要配置 MyBatis 的 SqlSessionFactoryBean:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="typeHandlersPackage" value="com.example"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
  • typeHandlersPackage 属性指定 TypeHandler 所在的包,MyBatis-Spring 会自动扫描并注册 TypeHandler。

这种方式的优点是与 Spring 集成,配置方便。 缺点是需要依赖 Spring 框架。

选择合适的注册方式

选择哪种注册方式取决于具体的应用场景和需求。

  • 如果需要为不同的属性使用不同的 TypeHandler,可以使用在 Mapper XML 文件中注册的方式。
  • 如果需要为某种 Java 类型设置全局默认的 TypeHandler,可以使用全局默认 TypeHandler 的方式。
  • 如果使用 MyBatis-Spring 集成,可以使用 Spring 的配置来注册 TypeHandler。
  • 如果希望配置集中,易于管理,可以使用在 MyBatis 配置文件中注册的方式。
  • 如果希望简洁,可以使用注解注册的方式。

建议根据项目的规模、复杂度和团队的习惯来选择合适的注册方式。

最佳实践

  • 尽量使用基于描述信息的 TypeHandler: 相比于基于 ordinal() 方法的 TypeHandler,基于描述信息的 TypeHandler 具有更好的可读性和维护性。
  • 保持 TypeHandler 的单一职责: 一个 TypeHandler 只负责处理一种 Java 类型和 JDBC 类型之间的转换。
  • 编写单元测试: 为 TypeHandler 编写单元测试,确保其正确性。
  • 避免在 TypeHandler 中进行复杂的业务逻辑: TypeHandler 只负责类型转换,不应该包含复杂的业务逻辑。
  • 合理选择注册方式: 根据具体的应用场景和需求选择合适的注册方式。
  • 考虑性能: 如果 TypeHandler 的逻辑比较复杂,可能会影响性能,需要进行性能测试和优化。

各种注册方式的对比

下面是一个表格,对比了各种注册方式的优缺点:

注册方式 优点 缺点 适用场景
MyBatis 配置文件中注册 配置集中,易于管理。 需要修改 MyBatis 配置文件,不够灵活。 适用于需要全局配置,且不需要为不同属性使用不同 TypeHandler 的场景。
Mapper XML 文件中注册 灵活,可以为不同的属性使用不同的 TypeHandler。 配置分散,不易于管理。 适用于需要为不同属性使用不同 TypeHandler 的场景。
使用注解注册 简洁,不需要在 Mapper XML 文件中指定 TypeHandler。 需要修改 MyBatis 配置文件,并且需要扫描包,可能会影响性能。 适用于希望简洁,且不需要为不同属性使用不同 TypeHandler 的场景。
全局默认 TypeHandler 简单,可以避免在 Mapper XML 文件中重复指定 TypeHandler。 缺乏灵活性,所有该类型的属性都会使用同一个 TypeHandler。 适用于需要为某种 Java 类型设置全局默认的 TypeHandler 的场景。
使用 MyBatis-Spring 集成 与 Spring 集成,配置方便。 需要依赖 Spring 框架。 适用于使用 MyBatis-Spring 集成的场景。

总结关键要点

本文详细介绍了 MyBatis 在处理枚举类型时可能遇到的问题,以及如何通过自定义 TypeHandler 来解决这些问题。 我们深入探讨了基于字符串和基于整数的 TypeHandler 的实现方式,并讲解了 MyBatis 中注册 TypeHandler 的各种方式,以及如何选择合适的注册方式。 希望本文能够帮助大家更好地理解 MyBatis 中枚举类型的处理方式,并在实际项目中应用。

灵活配置,类型安全,选择最适合你的方案

总之,MyBatis 提供了多种方式来处理枚举类型,开发者可以根据项目的具体需求选择最合适的方案。 关键在于理解 MyBatis 的工作原理,并根据实际情况进行灵活配置,以确保代码的可读性、可维护性和类型安全性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注