1.場景
日常java開發中經常有這種需求,用0或者1這些代碼(不局限於數字)來表示某種狀態。比如用0表示女性,用1來表示男性。而且寫入數據庫可能是一個標識,從數據庫讀取又還原為具體的說明。而且一般情況下為了更好理解或者消除魔法值,通常的處理方案是定義一個枚舉:
有些枚舉是這樣定義的
public enum GenderType{
FEMALE,MALE,UNKNOWN
}
那么通常很多人會這么入庫(java偽代碼)
if(GenderType.MALE){
// 寫入 1
}else if(GenderType.FEMALE){
// 寫入 0
}else{
//也可能是泰國回來的 那就 2
}
讀取的時候要么同樣按照上面的再反向處理一次或者使用數據庫sql語法case when
來直接寫入DTO
CASE gender
WHEN 1 THEN '男'
WHEN 0 THEN '女'
ELSE '未知' END
這種處理方式看起來不是很優雅。而且多了很多的判斷和處理邏輯,和我們的業務並不是非常相關。所以我們可以選擇更好的處理方式。
2.Mybatis中的TypeHandler
如果你ORM框架用的是Mybatis。那么將很容易通過TypeHandler接口解決這個問題。
2.1 TypeHandler 分析
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
源碼分析:
- setParameter 方法 通過 傳入的T類型寫你自己的邏輯,選擇調用 PreparedStatement 對象的某個set方法將數據寫入數據庫。此方法用來寫庫。
- getResult(ResultSet rs, String columnName) 通過字段名來讀庫並轉換為T類型。
- getResult(ResultSet rs, int columnIndex) 通過字段索引來讀庫並轉換為T類型。
- getResult(CallableStatement cs, int columnIndex) 調用存儲過程來獲取結果並轉換為T類型。
2.2 EnumOrdinalTypeHandler
我們發現TypeHandler有一個實現類EnumOrdinalTypeHandler。字面意思是可以通過枚舉的序號來處理類型。
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.ordinal());
}
我們先不考慮setNull的情況。通過此方法我們發現確實存入的是枚舉的順序值(順序從0開始),拿上面的例子來說 如果是GenderType.FEMALE是0,如果是GenderType.MALE是1,但是當GenderType.UNKNOWN時存入的是2。取的時候也是自然反向處理為具體的GenderType枚舉。
2.3 EnumTypeHandler
我們還發現有另外一個枚舉類型處理器。它的set方法是這樣的:
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}
我們不考慮jdbcType問題發現都是將Enum.name()的值寫入數據庫。拿上面的例子來說 如果是GenderType.FEMALE是FEMALE,如果是GenderType.MALE是MALE,但是當GenderType.UNKNOWN時存入的是UNKNOWN。讀庫是通過Enum.valueOf(Class enumType,String name)來進行反轉操作。
2.4 自定義TypeHandler
如果說我們的枚舉類型或者說我們使用其他方式來處理類別轉換怎么辦?當然Mybatis不會幫你干這么具體的事情。需要你自己來實現了。我們還拿枚舉作為例子,然后模仿上面的兩種TypeHandler。
還是拿開始的例子來說通常我個人比較喜歡這么定義枚舉:
public enum GenderTypeEnum {
/** * female. */
FEMALE(0, "女"),
/** * male. */
MALE(1,"男"),
/** * unknown. */
UNKNOWN(2, "未知");
private int value;
private String description;
GenderType(int value, String description) {
this.value = value;
this.description = description;
}
public int value() {
return this.value;
}
public String description() {
return this.description;
}
}
通過繼承BaseTypeHandler實現該抽象類的3個鈎子方法就行了:
@MappedTypes({GenderTypeEnum.class})
@MappedJdbcTypes({JdbcType.INTEGER})
public class GenderTypeEnumTypeHandler extends BaseTypeHandler<GenderTypeEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, GenderTypeEnum parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setInt(i, parameter.value());
} else {
// see r3589
ps.setObject(i, parameter.value(), jdbcType.TYPE_CODE);
}
}
@Override
public GenderTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
return getGenderType(rs.getInt(columnName));
}
@Override
public GenderTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return getGenderType(rs.getInt(columnIndex));
}
@Override
public GenderTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return getGenderType(cs.getInt(columnIndex));
}
private GenderTypeEnum getGenderType(int value) {
Class<GenderTypeEnum> genderTypeClass = GenderTypeEnum.class;
return Arrays.stream(genderTypeClass.getEnumConstants())
.filter(genderType -> genderType.value() == value)
.findFirst().orElse(GenderTypeEnum.UNKNOWN);
}
}
TypeHandler 實現寫好了,那么如何讓其發揮作用呢?我們接着往下走。
2.5 TypeHandler的核心要點
TypeHandler作用是javaType和jdbcType相互轉換。所以在聲明一個TypeHandler的時候一定要明確該TypeHandler處理的這兩種類型。這是必須要明確的原則。MyBatis不會通過窺探數據庫元信息來決定使用哪種JDBC類型,所以你必須在參數和結果映射中指明何種類型的字段,使其能夠綁定到正確的類型處理器上。MyBatis直到語句被執行時才清楚數據類型。
通過上述例子中的@MappedJdbcTypes和@MappedTypes來進行綁定類型轉換關系,也可以通過xml的typeHandler元素中的jdbcType或者javaType來指定。如果同時指定,xml的優先級要高。
注意有可能你會覆蓋內置的TypeHandler。所以自定義時一定要去了解Mybatis提供的一些默認處理器。避免對其他業務的影響。所以使用自定義TypeHandler很重要的一個原則就是一定要聲明JavaType和JdbcType.上面這些雖然比較生澀但是對於使用好TypeHandler非常重要。接下來我們來講講具體的配置。
2.6 免注冊TypeHandler
我們這里只講xml中的配置:
- 一種在rultMap元素中聲明一般用來查詢。一定要注意2.5中的一些原則。
<resultMap id="StudentMap" type="cn.felord.mybatis.entity.Student">
<id column="student_id" property="studentId"/>
<result column="student_name" property="studentName"/>
<result column="gender" property="genderType" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result column="age" property="age"/>
</resultMap>
- 然后是在插入、更新語句中使用。它們都是相同的,這里只舉一個插入例子。
<insert id="saveStu">
insert into student (student_name, gender, age)
values (#{studentName},
#{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER,typeHandler=cn.felord.mybatis.type.GenderTypeEnumTypeHandler},
#{age})
</insert>
如果注冊了別名都可以使用別名。上面的好處就是不用在TypeHandlerRegistry中進行注冊。
2.7 注冊TypeHandler
在配置中聲明注冊TypeHandler,然后Mybatis根據兩種類型會自動匹配。所以這里還是要強調2.5中的核心要點。
- 如果你是xml配置需要在Configuration配置文件中的標簽中進行聲明式注冊
<typeHandlers>
<typeHandler jdbcType="JdbcType枚舉存在的枚舉" javaType="typeAliases的別名或者全限定類名" handler="類全限定名"/>
<package name="指定所有typeHandler所在的包的包名"/>
</typeHandlers>
- javaConfig 方式 ,第一你可以通過SqlSessionFactory對象取到Configuration對象將typeHandler注冊進去。如果你使用mybatis-spring組件,可以在SqlSessionFactoryBean
的setTypeHandlersPackage方法中配置typeHandler的集中包路徑,那么框架將會自動掃描並注冊他們。springboot中對應的配置屬性是mybatis.typeHandlersPackage。
如果你注冊了TypeHandler。在Mapper.xml中只需要聲明jdbcType和javaType,無需再聲明具體的typeHandler。Mybatis會自動通過jdbcType、javaType來映射到具體注冊的TypeHandler上去 。就像下面的例子
<insert id="saveAutomaticStu">
insert into student (student_name, gender, age)
values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER}, #{age})
</insert>
3.總結
今天我們學習了mybatis開發中如何通過使用類型處理器進行類型的轉換處理,如何處理枚舉,如何自定義處理器並使用它。相信對你在java開發過程中會有很大的幫助。相關的代碼在我的碼雲倉庫中:https://gitee.com/felord/mybatis-test.git
多多關注我的公眾號 有更多干貨奉上
