如何在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個方法:
-
void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
用于定義設定參數時,該如何把Java類型的參數轉換為對應的資料庫類型
-
T getNullableResult(ResultSet rs, String columnName)
用于定義通過字段名稱擷取字段資料時,如何把資料庫類型轉換為對應的Java類型
-
T getNullableResult(ResultSet rs, int columnIndex)
用于定義通過字段索引擷取字段資料時,如何把資料庫類型轉換為對應的Java類型
-
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
的建立過程,來完成轉換器的指定。
思路
- 再寫一個能自動比對轉換行為的轉換器
- 通過
取得類型轉換器注冊器sqlSessionFactory.getConfiguration().getTypeHandlerRegistry()
- 再使用
将第一步的轉換器注冊成為預設的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中優雅的使用枚舉
請根據自己的配置修改