アレについて記す

Spring Boot(Hibernate)でEnumの値をDBに登録する方法について

Posted on February 06, 2015 at 06:00 (JST)

Spring Bootのデータ永続化にdata-jpaを使用している場合に、Enumの値をDBへ登録する方法について記載します。
Enum値の永続化方法は下記 4点になります。

  1. 何も指定せず、デフォルト挙動で登録
  2. @Enumeratedを付与して、nameかordinalを登録
  3. CustomUserTypeを作成して任意の値を登録
  4. TypeConverterを作成して任意の値を登録
今回は2と3の方法をサンプルに反映しました。
4の方法のサンプルはこちらをご覧ください。 作成したサンプルはGithubにて公開しています。[ simple-mvc-app ]

各指定方法は下記の通りです。

方式指定方法など
@Enumerated(EnumType.ORDINAL)対象フィールドに付与する。アノテーションを付与しない場合のデフォルトの挙動と同様。
DBに登録されている値に該当するEnumの列挙子が存在しない場合、例外となる。
Enumのordinalは列挙子の宣言順入れ替えにより変化するため、個人的にはお勧め出来ない。
@Enumerated(EnumType.STRING)対象フィールドに付与する。列挙子名(#nameで取得出来る結果)がDBに保存される。
こちらも、登録値に該当するEnumの列挙子が存在しない場合、例外となる。
CustomUserTypeorg.hibernate.usertype.UserTypeインターフェースを実装する
結構手間がかかるが、コード値など任意の値を登録することが可能。
該当する列挙子が存在しない場合の挙動も調整可能。
TypeConverterjavax.persistence.AttributeConverterインターフェースを実装する
CustomUserTypeより手軽に実装出来る。Java EEの標準仕様である点も嬉しい。
任意の値の登録・復元、および該当する列挙子が存在しない場合の挙動も調整可能。

ソース

まずはEntityクラスから。

[ Purchase.java(抜粋)]

@Entity
public class Purchase {

    @Id
    @GeneratedValue
    private Long id;

    @Type(type="com.example.app.purchase.GiftWrappingType")
    private GiftWrapping giftWrapping;

    @Enumerated(EnumType.STRING)
    private PaymentMethod paymentMethod;

    @Enumerated(EnumType.ORDINAL)
    private Prefecture prefecture;

    <省略>
}

@Enumeratedは見ての通り、対象に付与するだけで登録・復元が指定した通りとなります。
CustomUserTypeを使用する場合、@Typeを付与し作成したクラスをtypeに指定します。

つづいてGiftWrappingType。

[ GiftWrappingType.java ]

public class GiftWrappingType extends VarcharType {

    @Override
    public Class returnedClass() {
        return GiftWrapping.class;
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        String value = (String) StringType.INSTANCE.get(rs, names[0], session);
        return value == null
                ? null : GiftWrapping.of(value);
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        if (value == null) {
            StringType.INSTANCE.set(st, null, index, session);
        } else {
            StringType.INSTANCE.set(st, ((GiftWrapping)value).getCode(), index, session);
        }
    }
}

青字部分で任意のフィールド値の登録、および復元を行っています。
残念ながら、正当法ではEnum1つにつき対応するCustomeTypeを1つ作成する必要があります。。。
#nullSafeGetでResultSetのSQLからフィールド名を探って、、、みたいな力技以外で汎用的に使える方法をご存知でしたら、
是非是非教えてください!!

さて、気をとりなおしてVarcharTypeを見てみましょう。

[ VarcharType.java ]

public abstract class VarcharType implements UserType {

    @Override
    abstract public Class returnedClass();


    @Override
    abstract public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException;

    @Override
    abstract public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException;

    @Override
    public int[] sqlTypes() {
        return new int[] { VarcharTypeDescriptor.INSTANCE.getSqlType() };
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return x.equals(y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
}

VarcharTypeDescriptorStringTypeを使用することにより、文字列での登録となります。
DBのカラムを数値型にしたい場合、NumericTypeDescriptorIntegerTypeを使用すれば実現出来そうですが、未確認です。

最後に、GiftWrappingを見てみます。

[ GiftWrapping.java ]

public enum GiftWrapping {

    NONE("不要", "00"),
    CUTE("キュート柄", "10"),
    CHIC("シック柄", "20");

    private String label;
    private String code;

    private GiftWrapping(String label, String code) {
        this.label = label;
        this.code = code;
    }

    public String getLabel() {
        return label;
    }

    public String getCode() {
        return code;
    }

    public static GiftWrapping of(String code) {

        // UNDIFINEを用意して返したほうが良さげだけど、
        // Respos to BeanでNULLが入らなくするまではnullを返却
        if (code == null) { return null; }

        return Stream.of(values())
                .filter(type -> type.code.equals(code))
                .findFirst()
                .orElse(null);
    }
}

サンプルのECサイトっぽい機能で、ギフト用のラッピングを表すのに使用しているクラスです。
青字のメソッドが今回使用しているメソッドです。

CustomUserTypeを用意するのは結構手間がかかりますが、ドメイン駆動の値オブジェクトの永続化などに特に有効な手段だと思います。
特別な理由が無いのであれば、JPA2.1のTypeConverterを使用することをお勧めします。
なお、@Enumeratedも標準仕様のため、変換にこだわらないのであれば@Enumerated(EnumType.STRING)を使っておくのが安パイだと思います。
(この記事は@makingさんに4の方法があるとご教示いただき、2012/2/6 22:30に加筆修正しました。@makingさん、ありがとうございます!!)

以上です。