【解決方案】基於資料庫驅動的自定義 TypeHandler 處理器

CodeBlogMan發表於2024-10-09

目錄
  • 前言
  • 一、TypeHandler 簡介
    • 1.1轉換步驟
    • 1.2轉換規則
  • 二、JSON 轉換
  • 三、列舉轉換
  • 四、文章小結

前言

筆者在最近的專案開發中,頻繁地遇到了 Java 型別與 JDBC 型別之間的2個轉換問題:

  • 資料庫的 varchar 型別欄位,需要儲存 Java 實體中的 JSON 字串
  • 資料庫的 int 型別欄位,需要儲存 Java 實體中的 Enum 列舉

其實要處理也不麻煩,可以在每次入庫地方的手動將 Java Bean 呼叫 JSON.toJSONString() 即可,取出資料庫資料的時候再 JSON.parseObject()解析。再說處理列舉型別也並不難,無非就是手動將列舉的 int 型屬性取出後 set 到資料庫的int中去。

而本文要介紹的自定義 TypeHandler 處理器的作用,就是自動處理 Java Bean 與資料庫型別的轉換,提高編碼效率,透過全域性的統一處理省去繁瑣的手動轉換。


一、TypeHandler 簡介

如果我們使用的是 Mybatis 或者是 Mybatis Plus 的話,在 SQL 語句執行過程中,無論是設定引數還是獲取結果集,都需要透過 TypeHandler 進行型別轉換。

MyBatis 提供了豐富的內建 TypeHandler 實現,以支援常見的資料型別轉換,如以下幾種:

表1-1
【解決方案】基於資料庫驅動的自定義 TypeHandler 處理器

1.1轉換步驟

當 MyBatis 執行一個預編譯的 SQL 語句(如 INSERT、UPDATE 等)時,它需要將 Java 物件中的屬性值設定到 SQL 語句中對應的佔位符上。這個過程就是透過TypeHandler 來實現的。

具體步驟如下:

  • MyBatis 會根據對映配置找到對應的 TypeHandle r例項,這個對映配置可以在 MyBatis 的配置檔案或者 Mapper 的 XML 檔案中定義;
  • TypeHandler 例項會接收到 Java 物件中的屬性值,並將其轉換為 JDBC 能夠識別的型別,這個轉換過程是根據兩者之間的對映關係來實現的;
  • 轉換後的值會被設定到 PreparedStatement 物件中對應的佔位符上,以便資料庫能夠正確解析和執行 SQL 語句。

1.2轉換規則

再次強調,TypeHandler 的核心功能是實現 Java 型別和 JDBC 型別之間的對映和轉換,這個對映和轉換規則是根據 Java 型別和 JDBC 型別的特性和語義來定義的。

  • 對於基本資料型別(如 int、long、float等),MyBatis 提供了內建的 TypeHandler 實現,這些實現能夠直接將 Java 基本資料型別轉換為對應的 JDBC 基本資料型別,反之亦然。
  • 對於複雜資料型別(如自定義物件、集合等),MyBatis 允許開發者自定義 TypeHandler 來實現複雜的型別轉換邏輯。例如,開發者可以定義一個自定義的TypeHandler 來將資料庫中的 JSON 字串轉換為 Java 中的物件,或者將 Java 物件轉換為 JSON 字串儲存到資料庫中。

下面兩章就舉兩個例子來加以說明。


二、JSON 轉換

應用的 .yml 配置檔案新增以下:

mybatis-plus:
  type-handlers-package: #自定義 handler 類所在的包路徑
/**
 * <p>作用:即 Java 實體屬性可以直接使用 JSONObject 對映資料庫的 varchar,方便入庫、出庫</p>
 * <p>注意:需要在 .yml 配置檔案上加上 {@code mybatis:type-handlers-package: 本類所在包路徑}</p>
 *
 * @param <T> 該泛型即為需要轉換成 varchar 的 Java 物件
 * @MappedTypes 註解很關鍵,指定了對映的型別
 */
@MappedTypes({JSONObject.class, JSONArray.class})
public class JSONTypeHandler <T> extends BaseTypeHandler<T> {

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, T param, JdbcType jdbcType) throws SQLException {
        //將指定的引數設定為給定的 Java String 值,資料庫驅動程式及其轉換成 varchar 型別
        preparedStatement.setString(i, JSON.toJSONString(param));

    }

    @Override
    public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        //這裡根據欄位名去拿到之前放進來的 jsonStr 值
        String jsonStr = resultSet.getString(columnName);
        return StringUtils.isNotBlank(jsonStr) ? JSON.parseObject(jsonStr, getRawType()) : null;
    }

    @Override
    public T getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        //這裡是根據位置來確定欄位,進而拿到該欄位的值(之前放進來的 jsonStr)
        String jsonStr = resultSet.getString(columnIndex);
        return StringUtils.isNotBlank(jsonStr) ? JSON.parseObject(jsonStr, getRawType()) : null;
    }

    @Override
    public T getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        //這裡是根據SQL儲存過程裡的欄位位置來拿欄位的值
        String jsonStr = callableStatement.getString(columnIndex);
        return StringUtils.isNotBlank(jsonStr) ? JSON.parseObject(jsonStr, getRawType()) : null;
    }

}

三、列舉轉換

/**
 * <p>作用:將實體類中的列舉 code 對映為資料庫的 int</p>
 * <p>注意:需要在 .yml 配置檔案上加上 {@code mybatis:type-handlers-package: 本類所在包路徑}</p>
 *
 * @param <E> 該泛型即為需要處理的列舉物件,使用上界萬用字元來保證型別安全
 */
@MappedTypes(MyEnum.class)
public class EnumCodeTypeHandler <E extends MyEnum> extends BaseTypeHandler<E> {

    private final Class<E> type;

    /**
     * 記錄列舉值和列舉的對應關係
     */
    private final Map<Integer, E> enumMap = new ConcurrentHashMap<>();

    public EnumCodeTypeHandler(Class<E> type) {
        Assert.notNull(type, "argument cannot be null");
        this.type = type;
        E[] enums = type.getEnumConstants();
        if (Objects.nonNull(enums)) {
            //這裡將列舉值和列舉型別存入 enumMap
            for (E e : enums) {
                this.enumMap.put(e.toCode(), e);
            }
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int index, E e, JdbcType jdbcType) throws SQLException {
        //這裡將列舉的 code 轉為資料庫該欄位的 int 型別
        preparedStatement.setInt(index, e.toCode());
    }

    @Override
    public E getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        //這裡根據欄位名來將資料庫的 int 轉為 Java 的 Integer
        Integer code = resultSet.getInt(columnName);
        if (resultSet.wasNull()){
            return null;
        }else {
            //取出對應的列舉值
            return enumMap.get(code);
        }
    }

    @Override
    public E getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        //這裡根據欄位位置來將資料庫的 int 轉為 Java 的 Integer
        Integer code = resultSet.getInt(columnIndex);
        if (resultSet.wasNull()){
            return null;
        }else {
            //取出對應的列舉值
            return enumMap.get(code);
        }
    }

    @Override
    public E getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        //這裡根據SQL儲存過程裡的欄位位置將欄位的 int 轉為 Java 的 Integer
        Integer code = callableStatement.getInt(columnIndex);
        if (callableStatement.wasNull()){
            return null;
        }else {
            //取出對應的列舉值
            return enumMap.get(code);
        }
    }

}

/**
 * <p>作用:該介面包含了兩個列舉操作的抽象方法</p>
 */
public interface MyEnum {

    /**
     * 根據 code 獲取列舉例項
     * @param code
     */
    MyEnum fromCode(int code);

    /**
     * 獲取列舉中的 code
     */
    int toCode();

}
@Getter
@RequiredArgsConstructor
public enum StudyStatusEnum implements MyEnum{
    ONE(1, "列舉1"),
    TWO(2, 列舉2"),
    THREE(3, "列舉3"),
    FOUR(4, "列舉4"),
    FIVE(5, "列舉5");

    private final Integer code;

    private final String desc;

    /**
     * 根據 code 獲取列舉例項
     */
    @Override
    public MyEnum fromCode(int code) {
        return Arrays.stream(StudyStatusEnum.values())
                .filter(val -> val.getCode().equals(code))
                .findFirst().orElse(null);
    }

    /**
     * 獲取列舉中的 code
     */
    @Override
    public int toCode() {
        return this.getCode();
    }

}

四、文章小結

透過內建和自定義的 TypeHandler,我們可以輕鬆處理各種資料型別轉換需求,提高開發效率和程式碼可維護性。

在 Spring Boot 環境中使用自定義 TypeHandler 更是簡化了配置和註冊過程,使得我們能夠更專注於業務邏輯的實現。

最後,文章如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!

相關文章