使用枚举或其他对象

在 sys_role 表中存在一个字段 enabled,这个字段只有两个可选值,0 为禁用,1 为启用。但是在 SysRole 类中,我们使用的是 Integer enabled,这种情况下必须手动校验 enabled 的值是否符合要求。在只有两个值的情况下,处理起来还比较容易,但是当出现更多的可选值时,对值进行校验就会变得复杂。因此在这种情况下,我们通常会选择使用枚举来解决。

使用MyBatis提供的枚举处理器

在 tk.mybatis.simple.type 包中新增 Enabled 枚举类,代码如下。

public enum Enabled {
    /**
     * 禁用(对应索引为 0)
     */
    disabled,
    /**
     * 启用(对应索引为 1)
     */
    enabled;

}

因为枚举除了本身的字面值外,还可以通过枚举的 ordinal() 方法获取枚举值的索引。在这个枚举类中,disabled 对应索引 0,enabled 对应索引 1。

增加枚举后,修改 SysRole 中 enabled 的类型,部分修改后的代码如下。

import jdk.jfr.Enabled; /**
 * 有效标志
 */
private Enabled enabled;

    /**
 * 获取 有效标志
 *
 * @return enabled 有效标志
 */
public Enabled getEnabled() {
    return this.enabled;
}

/**
 * 设置 有效标志
 *
 * @param enabled 有效标志
 */
public void setEnabled(Enabled enabled) {
    this.enabled = enabled;
}

将 enabled 改为枚举类型后,可选值的问就解决了,在 Java 中处理该值也变得简单了。但是这个值该如何和数据库的值进行交互呢?

在数据库中不存在一个和 Enabled 枚举对应的数据库类型,因此在和数据库交互的时候,不能直接使用枚举类型,在查询数据时,需要将数据库 int 类型的值转换为 Java 中的枚举值。在保存、更新数据或者作为查询条件时,需要将枚举值转换为数据库中的 int 类型。

MyBatis 在处理 Java 类型和数据库类型时,使用 TypeHandler(类型处理器)对这两者进行转换。Mybatis 为 Java 和数据库 JDBC 中的基本类型和常用的类型提供了 TypeHandler 接口的实现。MyBatis 在启动时会加载所有的 JDBC 对应的类型处理器,在处理枚举类型时默认使用 org.apache.ibatis.type.EnumTypeHandler 处理器,这个处理器会将枚举类型转换为字符串类型的字面值并使用,对于 Enabled 而言便是 "disabled" 和 "enabled" 字符串。在这个例子中,由于数据库使用的是 int 类型,所以在 Java 的 String 类型和数据库 int 类型互相转换时,肯定会报错。

使用第 3 章中针对 SysRole 的 selectById 和 updateById 方法编写一个测试,在 RoleMapperTest 中添加如下测试。

@Test
public void testUpdateById(){
    //获取 sqlSession
    SqlSession sqlSession = getSqlSession();
    try {
        //获取 RoleMapper 接口
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        //由于数据库数据 enable 都是 1,所以我们给其中一个角色的 enable 赋值为 0
        SysRole role = roleMapper.selectById(2L);
        Assert.assertEquals(Enabled.enabled, role.getEnabled());
        role.setEnabled(Enabled.disabled);
        roleMapper.updateById(role);
    } finally {
        sqlSession.rollback();
        //不要忘记关闭 sqlSession
        sqlSession.close();
    }
}

编写完这个测试后,直接执行,抛出如下的异常信息。

这个错误原因是,在调用 Enabled.valueOf("1") 的时候,枚举中没有 1 这个枚举值。因为 MyBatis 默认使用 org.apache.ibatis.type.EnumTypeHandler,这个处理器只是对枚举的字面值进行处理,所以不适合当前的情况。除了这个枚举类型处理器,MyBatis 还提供了另一个 org.apache.ibatis.type.EnumOrdinalTypeHandler 处理器,这个处理器使用枚举的索引进行处理,可以解决此处遇到的问题。想要使用这个处理器,需要在 mybatis-config.xml 中添加如下配置。

<typeHandlers>
    <typeHandler javaType="cn.liaozh.mybatis2.ch6.query.type.Enabled" handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
</typeHandlers>

在 typeHandler 中,通过 javaType 设置要处理的枚举类型,通过 handler 设置类型处理器。做好这些配置后,再执行上面的测试,输出的日志如下。

从第一个方法查询的返回值可以看到,MyBatis 将 1 处理为 enabled。在第二个更新方法中,MyBatis 将 disabled 处理为 0 来更新数据库。通过 typeHandler 配置就实现了 Java 类型和 JDBC 类型的互相转换。

使用自定义的类型处理器

上面的配置解决了枚举问题,但有的时候,值既不是枚举的字面值,也不是枚举的索引值,这种情况下就需要自己来实现类型处理器了。简单修改枚举类 Enabled,代码如下。

public enum Enabled {

    /**
     * 启用
     */
    enabled(1),
    /**
     * 禁用
     */
    disabled(0);

    private final int value;

    EnabledType(int value){
        this.value = value;
    }
    public  int getValue(){
        return value;
    }
}

现在 Enabled 中的值和顺序无关,针对该类,在 tk.mybatis.simple.type 包下新增 EnabledTypeHandler 类,代码如下。

package cn.liaozh.mybatis2.ch6.query.type;

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

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * EnabledType 类型处理器
 **/
public class EnabledTypeHandler implements TypeHandler<EnabledType>{

    private final Map<Integer,EnabledType> enabledMap = new HashMap<Integer,EnabledType>(EnabledType.values().length);

    public EnabledTypeHandler(){
        for (EnabledType enabledType : EnabledType.values()) {
            enabledMap.put(enabledType.getValue(),enabledType);
        }
    }

    @Override
    public void setParameter(PreparedStatement ps, int i, EnabledType parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i,parameter.getValue());
    }

    @Override
    public EnabledType getResult(ResultSet rs, String columnName) throws SQLException {
        Integer value = rs.getInt(columnName);
        return enabledMap.get(value);
    }

    @Override
    public EnabledType getResult(ResultSet rs, int columnIndex) throws SQLException {
        Integer value = rs.getInt(columnIndex);
        return enabledMap.get(value);
    }

    @Override
    public EnabledType getResult(CallableStatement cs, int columnIndex) throws SQLException {
        Integer value = cs.getInt(columnIndex);
        return enabledMap.get(value);
    }
}

EnabledTypeHandler 实现了 TypeHandler 接口,并且针对 4 个接口方法对 Enabled 类型进行了转换。在 TypeHandler 接口实现类中,除了默认无参的构造方法,还有一个隐含的带有一个 Class 参数的构造方法。

public EnabledTypeHandler(Class<?> type) {
    this();
}

当针对特定的接口处理类型时,使用这个构造方法可以写出通用的类型处理器,就像 MyBatis 提供的两个枚举类型处理器一样。有了自己的类型处理器后,还需要在 mybatis-config.xml 中进行如下配置。

<typeHandlers>
    <typeHandler javaType="cn.liaozh.mybatis2.ch6.query.type.EnabledType" handler="cn.liaozh.mybatis2.ch6.query.type.EnabledTypeHandler"/>
</typeHandlers>

修改后再次执行测试方法,测试会正确执行。这里只是实现了一个简单的类型处理器,如果需要用到复杂的类型处理,可以参考 MyBatis 项目中 org.apache.ibatis.type 包下的各种类型处理器的实现。

对Java 8日期(JSR-310)的支持

MyBatis 从 3.4.0 版本开始增加了对 Java 8 日期(JSR-310)的支持。如果使用 3.4.0 及以上版本,只需要在 Maven 的 pom.xml 中添加如下依赖即可。

<!--对Java 8 日期(JSR-310)的支持,当MyBatis是3.4.0以上版本时,添加以下依赖即可-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-typehandlers-jsr310</artifactId>
    <version>1.0.2</version>
</dependency>

关于日期类型处理器的版本,可以参考该项目在 GitHub 首页的文档,地址是 https://github.com/mybatis/typehandlers-jsr310

如果使用比 3.4.0 更早的版本,若要支持 Java 8 日期,还需要在 mybatis-config.xml 中添加如下配置。

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.InstantTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.LocalDateTimeTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.LocalDateTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.LocalTimeTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.OffsetDateTimeTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.OffsetTimeTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.ZonedDateTimeTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.YearTypeHandler" />
    <typeHandler handler="org.apache.ibatis.type.MonthTypeHandler" />
</typeHandlers>

增加上面这些配置后,就可以在 Java 中使用新的日期类型了。

在看到上面日期的 typeHandler 配置时,大家有没有一点疑问呢?为什么这些 typeHandler 都没有配置 javaType 呢?来看看 InstantTypeHandler 类的源码。

public class InstantTypeHandler extends BaseTypeHandler<Instant> {
    // 其他方法
}

InstantTypeHandler 并不像上面的 EnabledTypeHandler 实现的 TypeHandler 接口一样,InstantTypeHandler 继承了 BaseTypeHandler<T> 类,而 BaseTypeHandler<T> 又继承了 TypeReference<T> 类。由于 TypeReference<T> 带有泛型类型,MyBatis 会对继承了 TypeReference<T> 的类进行特殊处理,获取这里指定的泛型类型作为 javaType 属性,因此在配置的时候就不需要指定 javaType 了。