1. 程式人生 > >玩轉mybatis中的型別轉換

玩轉mybatis中的型別轉換

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<T>介面解決這個問題。

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時存入的是3。取的時候也是自然反向處理為具體的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<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