1. ホーム
  2. Java

MyBatisカスタムタイプハンドラ TypeHandler

2022-02-19 18:32:15
<パス

プロジェクト開発でよく問題になるのが

javabeanで列挙型などをカスタマイズしてデータベースに格納する場合、対応するデータベースの型に変換する必要があり、データベースから取り出す際にもデータベースの型をjavabeanの対応する型に変換する必要があることが多いのです。例えば、javabean のフィールド型が Date であれば、データベースは varchar として格納し、javabean のフィールド型が Enum であれば、データベースは String または Integer として格納します。
MyBatisは解決策を提供してくれています。TypeHandlerタイプハンドラです。

タイプハンドラ TypeHandler

MyBatis のタイプハンドラ TypeHandler は、JavaType と JdbcType 間の変換、PreparedStatement のパラメータ値の設定、ResultSet や CallableStatement からの値の取得に使用されます。MyBatisには、基本的な型のほとんどは、組み込みのタイプハンドラがあるので、直接扱うことができますが、その他の型を扱う必要がある場合は、カスタムのタイプハンドラが必要です。

MyBatis の組み込み TypeHandler

MyBatis の TypeHandlerRegistry タイプで、組み込みのタイプハンドラを見ることができます。組み込みのハンドラはもっとたくさんあるので、ここでは一般的なものをリストアップしています。
BooleanTypeHandler: java型boolean、jdbc型bit、boolean用
ByteTypeHandler: Java型byte、jdbc型TINYINT用
ShortTypeHandler:Javaのshort型、jdbc type SMALLINTに対応。
IntegerTypeHandler: INTEGER型用
LongTypeHandler:long型用
FloatTypeHandler:FLOAT型用
DoubleTypeHandler: double型用
StringTypeHandler: Java型文字列、jdbc型CHAR、VARCHAR用
ArrayTypeHandler: JdbcタイプARRAY用
BigDecimalTypeHandler: Java型BigDecimal、jdbc型REAL, DECIMAL, NUMERIC用
DateTypeHandler: Java型Date、jdbc型TIMESTAMP用
DateOnlyTypeHandler: Java型Date, jdbc型DATE用
TimeOnlyTypeHandler: Java型Date, jdbc型TIME用
一般的なEnum型については、Enum名変換のための組み込みEnumTypeHandlerとEnum序数変換のためのEnumOrdinalTypeHandlerがあります。この2つのタイプハンドラはTypeHandlerRegistryに登録されていないため、使用する必要がある場合は手動で設定する必要がある。

TypeHandlerのカスタマイズ

カスタムのタイプハンドラは org.apache.ibatis.type.TypeHandler インターフェースを実装することで実現されます。このインターフェイスはタイプハンドラの基本的な機能を定義しており、そのインターフェイスの定義を以下に示します。

setParameterメソッドはPreparedStatementのパラメータにjavaオブジェクトを設定するために使用され、getResultメソッドはResultSet(列名またはインデックス位置に基づく)またはCallableStatement(ストアドプロシージャに基づく)からjavaオブジェクトにデータを取得するために使用されています。

実際には、org.apache.ibatis.typeを継承して、カスタムタイプハンドラを実装することができます。この型は、汎用処理をカプセル化し、例外処理を行うTypeHandlerのメソッドを実装し、以下のようにいくつかの類似した抽象メソッドを定義した抽象型です。BaseTypeHandler型を継承することで、開発工数を大幅に削減することができます。


タイプコンバータは、アノテーションによってjavaタイプやjdbcタイプを設定することもできます。

MappedTypes: Java の型を設定するためのアノテーションです。
MappedJdbcTypes: Jdbcの型を設定するためのアノテーション

カスタム列挙型ハンドラの例です。

列挙型の基底クラスをカスタマイズする

import org.springframework.util.ReflectionUtils;
import java.lang.reflection.Field;

/**
 * @author: liumengbing
 * @date: 2019/05/20 15:37
 **/
public interface BaseEnum {

    String DEFAULT_VALUE_NAME = "value";

    String DEFAULT_LABEL_NAME = "label";

    default Integer getValue() {
        Field field = ReflectionUtils.findField(this.getClass(), DEFAULT_VALUE_NAME);
        if (field == null)
            return null;
        try {
            field.setAccessible(true);
            return Integer.parseInt(field.get(this).toString());
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    default String getLabel() {
        Field field = ReflectionUtils.findField(this.getClass(), DEFAULT_LABEL_NAME);
        if (field == null)
            return null;
        try {
            field.setAccessible(true);
            return field.get(this).toString();
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    static <T extends Enum<T>> T valueOfEnum(Class<T> enumClass, Integer value) {
        if (value == null)
            throw new IllegalArgumentException("DisplayedEnum value should not be null");
        if (enumClass.isAssignableFrom(com.zfkr.qianyue.common.enums.BaseEnum.class))
            throw new IllegalArgumentException("illegal DisplayedEnum type");
        T[] enums = enumClass.getEnumConstants();
        for (T t: enums) {
            com.zfkr.qianyue.common.enums.BaseEnum displayedEnum = (com.zfkr.qianyue.common.enums.BaseEnum)t;
            if (displayedEnum.getValue().equals(value))
                return (T) displayedEnum;
        }
        throw new IllegalArgumentException("cannot parse integer: " + value + " to " + enumClass.getName());
    }

    static <T> T valueOfEnum1(T[] enums, Integer value) {

        for (T t: enums) {
            com.zfkr.qianyue.common.enums.BaseEnum displayedEnum = (com.zfkr.qianyue.common.enums.BaseEnum)t;
            if (displayedEnum.getValue().equals(value))
                return (T) displayedEnum;
        }
        throw new IllegalArgumentException("cannot parse integer: " + value + " to " );
    }
}



列挙型クラス Enum1 を定義する

/**
 * @author: liumengbing
 * @date: 2019/05/20 15:34
 **/
public enum Enum1 implements BaseEnum{

    UNAUDITED("not audited",0),AUDIT("to be audited",1),AUDITED("audited",2);

    private String label;

    private Integer value;

    Enum1(String label,Integer value){
        this.label = label;
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }

    public String getLabel() {
        return label;
    }

    public static Enum1 getByValue(Integer value){
        for(Enum1 enum1 : values()){
            if (enum1.getValue() == value) {
                return enum1;
            }
        }
        return null;
    }

}


カスタム列挙型ハンドラ

/**
 * @author: liumengbing
 * @date: 2019/05/20 15:34
 **/
@MappedTypes(value = { Enum1.class, Enum2.class})
public class EnumTypeHandler extends BaseTypeHandler<BaseEnum> {

    private Class<BaseEnum> type;

    public EnumTypeHandler() {
    }

    public EnumTypeHandler(Class<BaseEnum> type) {
        if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, BaseEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getValue());
    }

    @Override
    public BaseEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return convert(rs.getInt(columnName));
    }

    @Override
    public BaseEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return convert(rs.getInt(columnIndex));
    }

    @Override
    public BaseEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return convert(cs.getInt(columnIndex));
    }

    private BaseEnum convert(int status) {
        BaseEnum[] objs = type.getEnumConstants();
        for (BaseEnum em : objs) {
            if (em.getValue() == status) {
                return em;
            }
        }
        return null;
    }
}


Javabean、Dao層、Mapper層の処理は無視し、アプリケーションにカスタムタイプハンドラを設定するだけです。

TypeHandlerをアプリケーションに設定するには、3つの方法があります。

1. Mapper.xmlで宣言する。

<result column="enum1" jdbcType="INTEGER" property="enum1" typeHandler="com.xxx.handler. EnumTypeHandler"/>


2. mybatis設定ファイルでの設定

<typeHandlers>
        <typeHandler handler="com.xxx.handler.EnumTypeHandler"/>
    </typeHandlers>


3. springbootの設定ファイルymlにタイプハンドラのあるパッケージ名を設定します。

mybatis:
  type-handlers-package: com.xxx.handler


ソースコード解析

MyBatisが起動し、まずtypeHandlerを登録する。まずMappedTypesアノテーションを読み込んでみて、このアノテーションで定義されたjava型があれば、それに対応するjava型ハンドラに登録する。アノテーションがなく、前述のBaseTypeHandlerのようにTypeReference型を継承している場合は、TypeReferenceインターフェースを通して元の型を取得し、対応するjava型処理系に登録する。javaの型が利用できない場合は、型付けされていないものとして扱われる。

public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandler.getClass()
        .getAnnotation(MappedTypes.class);
    if (mappedTypes ! = null) {
      for (Class<? > handledType : mappedTypes.value()) {
        register(handledType, typeHandler);
        mappedTypeFound = true;
      }
    }
    // @since 3.1.0 - try to auto-discover the mapped type
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
      try {
        TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
        register(typeReference.getRawType(), typeHandler);
        mappedTypeFound = true;
      } catch (Throwable t) {
        // maybe users define the TypeReference with a different type and 
are not assignable, so just ignore it
      }
    }
    if (!mappedTypeFound) {
      register((Class<T>) null, typeHandler);
    }
  }


MyBatisは、前処理文にパラメータを設定する際に、TypeHandlerを呼び出し、JavaオブジェクトをjdbcのPreparedStatementのパラメータ値に変換しています。以下は、その呼び出しの1つのスニペットです。

TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
    jdbcType = configuration.getJdbcTypeForNull();
}
try {
    typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
    throw new TypeException("Could not set parameters for mapping: " 
    + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
    throw new TypeException("Could not set parameters for mapping: " 
    + parameterMapping + ". Cause: " + e, e);
}



MyBatisによるデータベースへの問い合わせが完了すると、TypeHandlerメソッドが呼び出され、データがjavaオブジェクトに読み込まれます。以下は、その呼び出しの1つのスニペットである。

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, 
  ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
  throws SQLException {
    if (propertyMapping.getNestedQueryId() ! = null) {
        return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, 
            lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() ! = null) {
        addPendingChildRelation(rs, metaResultObject, propertyMapping);   
        // TODO is that OK?
        return DEFERED;
    } else {
        final TypeHandler<? > typeHandler = propertyMapping.getTypeHandler();
        final String column = prependPrefix(propertyMapping.getColumn(), 
            columnPrefix);
        return typeHandler.getResult(rs, column);
    }
}


参考
mybatisにおけるいくつかのtypeHandlerの使い方の定義
MyBatisタイプハンドラ TypeHandler