玩轉mybatis中的類型轉換

1.場景

平常java開發中常常有這種需求,用0或者1這些代碼(不侷限於數字)來表示某種狀態。好比用0表示女性,用1來表示男性。並且寫入數據庫多是一個標識,從數據庫讀取又還原爲具體的說明。並且通常狀況下爲了更好理解或者消除魔法值,一般的處理方案是定義一個枚舉:java

有些枚舉是這樣定義的git

public enum GenderType{
      FEMALE,MALE,UNKNOWN
 }

那麼一般不少人會這麼入庫(java僞代碼)spring

if(GenderType.MALE){
   // 寫入 1
  }else if(GenderType.FEMALE){
   // 寫入 0
  }else{
  //也多是泰國回來的 那就 2
  }

讀取的時候要麼一樣按照上面的再反向處理一次或者使用數據庫sql語法case when 來直接寫入DTOsql

CASE gender
 WHEN 1 THEN '男'
 WHEN 0 THEN '女'
 ELSE '未知' END

這種處理方式看起來不是很優雅。並且多了不少的判斷和處理邏輯,和咱們的業務並非很是相關。因此咱們能夠選擇更好的處理方式。數據庫

2.Mybatis中的TypeHandler

若是你ORM框架用的是Mybatis。那麼將很容易經過TypeHandler<T>接口解決這個問題。apache

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;

}

源碼分析:springboot

  • 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。字面意思是能夠經過枚舉的序號來處理類型。mybatis

@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枚舉。app

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<T> 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>標籤中進行聲明式註冊
<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

多多關注個人公衆號:碼農小胖哥,能夠獲得更加及時的資訊和反饋。

相關文章
相關標籤/搜索