天天看點

如何在Mybatis中優雅的使用枚舉如何在Mybatis中優雅的使用枚舉如何在MyBatis中優雅的使用枚舉

如何在Mybatis中優雅的使用枚舉

問題

在編碼過程中,經常會遇到用某個數值來表示某種狀态、類型或者階段的情況,比如有這樣一個枚舉:

public class QuestionnaireEnum {

    /**
     * 學曆
     */
    public enum Education{
        Education_Junior(0, "國中"),
        Education_Senior(1, "高中"),
        Education_College(2, "大學/大專"),
        Education_Master(3, "碩士"),
        Education_Doctor(4, "博士"),
        Education_Other(5, "其他");
        private Integer value;
        private String label;

        /*省略get和set代碼*/
    }

    /**
     * 目前的職業
     */
    public enum Job{
        Job_Teacher(0, "教師"),
        Job_Medic(1, "醫護"),
        Job_Civil(2, "公務員"),
        Job_Worker(3, "務工"),
        Job_Soldier(4, "軍人"),
        Job_Other(5, "其他");
        /*省略get和set代碼*/
}
           

通常我們希望将表示狀态的數值存入資料庫,即

ComputerState.OPEN

存入資料庫取值為

10

探索

首先,我們先看看MyBatis是否能夠滿足我們的需求。

MyBatis内置了兩個枚舉轉換器分别是:

org.apache.ibatis.type.EnumTypeHandler

org.apache.ibatis.type.EnumOrdinalTypeHandler

EnumTypeHandler

這是預設的枚舉轉換器,該轉換器将枚舉執行個體轉換為執行個體名稱的字元串,即将

ComputerState.OPEN

轉換

OPEN

EnumOrdinalTypeHandler

顧名思義這個轉換器将枚舉執行個體的ordinal屬性作為取值,即

ComputerState.OPEN

轉換為

,

ComputerState.CLOSE

轉換為

1

使用它的方式是在MyBatis配置檔案中定義:

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>
           

以上的兩種轉換器都不能滿足我們的需求,是以看起來要自己編寫一個轉換器了。

方案

MyBatis提供了

org.apache.ibatis.type.BaseTypeHandler

類用于我們自己擴充類型轉換器,上面的

EnumTypeHandler

EnumOrdinalTypeHandler

也都實作了這個接口。

1. 定義接口

我們需要一個接口來确定某部分枚舉類的行為。如下:

public interface IBaseEnum{
    Integer getValue();
    String getLabel();
}
           

該接口隻有一個傳回編碼的方法,傳回值将被存入資料庫。

2. 改造枚舉

就拿上面的

ComputerState

來實作

BaseCodeEnum

接口:

public class QuestionnaireEnum {

    /**
     * 學曆
     */
    public enum Education implements IBaseEnum {
        Education_Junior(0, "國中"),
        Education_Senior(1, "高中"),
        Education_College(2, "大學/大專"),
        Education_Master(3, "碩士"),
        Education_Doctor(4, "博士"),
        Education_Other(5, "其他");
        /*省略get和set代碼*/
    }

    /**
     * 目前的職業
     */
    public enum Job implements IBaseEnum {
        Job_Teacher(0, "教師"),
        Job_Medic(1, "醫護"),
        Job_Civil(2, "公務員"),
        Job_Worker(3, "務工"),
        Job_Soldier(4, "軍人"),
        Job_Other(5, "其他");
        /*省略get和set代碼*/
}
           

3. 編寫一個轉換工具類

現在我們能順利的将枚舉轉換為某個數值了,還需要一個工具将數值轉換為枚舉執行個體。

public class QuestEnumUtil {
    public static <E extends Enum<?> & IBaseEnum> E codeOf(Class<E> enumClass, int value) {
        E[] enumConstants = enumClass.getEnumConstants();
        for (E e : enumConstants) {
            if (e.getValue() == value)
                return e;
        }
        return null;
    }
}
           

4. 自定義類型轉換器

準備工作做的差不多了,是時候開始編寫轉換器了。

BaseTypeHandler<T>

 一共需要實作4個方法:

  1. void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)

用于定義設定參數時,該如何把Java類型的參數轉換為對應的資料庫類型

  1. T getNullableResult(ResultSet rs, String columnName)

用于定義通過字段名稱擷取字段資料時,如何把資料庫類型轉換為對應的Java類型

  1. T getNullableResult(ResultSet rs, int columnIndex)

用于定義通過字段索引擷取字段資料時,如何把資料庫類型轉換為對應的Java類型

  1. T getNullableResult(CallableStatement cs, int columnIndex)

用定義調用存儲過程後,如何把資料庫類型轉換為對應的Java類型

我是這樣實作的:

public class QuestEnumTypeHandler<E extends Enum<?> & IBaseEnum> extends BaseTypeHandler<IBaseEnum> {

    private Class<E> type;
    private E[] enums;
    public QuestEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
    }

    private E codeOf(int code){
        try {
            return QuestEnumUtil.codeOf(type, code);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex);
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, IBaseEnum iBaseEnum, JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i,iBaseEnum.getValue());
    }

    @Override
    public IBaseEnum getNullableResult(ResultSet resultSet, String s) throws SQLException {
        int anInt = resultSet.getInt(s);
        return resultSet.wasNull()?null:codeOf(anInt);
    }

    @Override
    public IBaseEnum getNullableResult(ResultSet resultSet, int i) throws SQLException {
        int anInt = resultSet.getInt(i);
        return resultSet.wasNull()?null:codeOf(anInt);
    }

    @Override
    public IBaseEnum getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        int anInt = callableStatement.getInt(i);
        return callableStatement.wasNull()?null:codeOf(anInt);
    }
}
           

5. 使用

接下來需要指定哪個類使用我們自己編寫轉換器進行轉換,在MyBatis配置檔案中配置如下:

<typeHandlers>
    <typeHandler handler="xxx.xxx.xxx.QuestEnumTypeHandler" javaType="xxx.xxx.xxx.QuestionnaireEnum"/>
</typeHandlers>
           

搞定! 經測試

ComputerState.OPEN

被轉換為

10

,

ComputerState.UNKNOWN

被轉換為

255

,達到了預期的效果。

6. 優化

在第5步時,我們在MyBatis中添加

typeHandler

用于指定哪些類使用我們自定義的轉換器,一旦系統中的枚舉類多了起來,MyBatis的配置檔案維護起來會變得非常麻煩,也容易出錯。如何解決呢?

Spring

中我們可以使用

JavaConfig

方式來幹預

SqlSessionFactory

的建立過程,來完成轉換器的指定。

思路

  1. 再寫一個能自動比對轉換行為的轉換器
  2. 通過

    sqlSessionFactory.getConfiguration().getTypeHandlerRegistry()

    取得類型轉換器注冊器
  3. 再使用

    typeHandlerRegistry.setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler)

    将第一步的轉換器注冊成為預設的

首先,我們需要一個能确定轉換行為的轉換器:

AutoEnumTypeHandler.java

public class AutoEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private BaseTypeHandler typeHandler = null;

    public AutoEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        if(IBaseEnum.class.isAssignableFrom(type)){
            // 如果實作了 BaseCodeEnum 則使用我們自定義的轉換器
            typeHandler = new QuestEnumTypeHandler(type);
        }else {
            // 預設轉換器 也可換成 EnumOrdinalTypeHandler
            typeHandler = new EnumTypeHandler<>(type);
        }
    }


    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        typeHandler.setNonNullParameter(preparedStatement, i, e, jdbcType);
    }

    @Override
    public E getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return (E) typeHandler.getNullableResult(resultSet, s);
    }

    @Override
    public E getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return (E) typeHandler.getNullableResult(resultSet, i);
    }

    @Override
    public E getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return (E) typeHandler.getNullableResult(callableStatement,i);
    }
}
           

接下來,我們需要幹預

SqlSessionFactory

的建立過程,将剛剛的轉換器指定為預設的:

@Configuration
@ConfigurationProperties(prefix = "mybatis")
public class MyBatisConfig {
    private String configLocation;

    private String mapperLocations;

    public String getConfigLocation() {
        return configLocation;
    }

    public void setConfigLocation(String configLocation) {
        this.configLocation = configLocation;
    }

    public String getMapperLocations() {
        return mapperLocations;
    }

    public void setMapperLocations(String mapperLocations) {
        this.mapperLocations = mapperLocations;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);


        // 設定配置檔案及mapper檔案位址
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setConfigLocation(resolver.getResource(configLocation));
        factory.setMapperLocations(resolver.getResources(mapperLocations));


        SqlSessionFactory sqlSessionFactory = factory.getObject();


        // 取得類型轉換注冊器
        TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
        // 注冊預設枚舉轉換器
        typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);

        return sqlSessionFactory;
    }
}
           

搞定! 這樣一來,如果枚舉實作了

BaseCodeEnum

接口就使用我們自定義的

CodeEnumTypeHandler

,如果沒有實作

BaseCodeEnum

接口就使用預設的。再也不用寫MyBatis的配置檔案了!

就以為這樣子就解決了嗎?别忘了在相關的mybati.xml檔案設定映射

<resultMap type="" id="">
<result column="q_education" property="QEducation" typeHandler="com.hjr.enums.QuestEnumTypeHandler"/>
<result column="q_job" property="QJob" typeHandler="com.hjr.enums.QuestEnumTypeHandler"/>
</resultMap>
           

如何在MyBatis中優雅的使用枚舉

請根據自己的配置修改