読者です 読者をやめる 読者になる 読者になる

JPAで値付きのEnumを扱う

JPA Java DB [Enum]

JPA実装によって話は変わってくるかもしれません.わたしはEclipseLinkを使っています.

JPAEnumを扱うとき

@Enumerated(EnumType.String)
private HogeEnum hoge;

ふつうはこうすると思います.
@Enumeratedアノテーションを列挙型の変数につけると,JPAがpersistしてDBにinsertするときに,EnumTypeの値にしたがって変換してくれます.
Stringを指定すると,Enum#name,Ordinalを指定すると,Enumを指定するとEnum#ordinalをそれぞれ呼んだ結果に変換されて永続化されます.
DBの値からエンティティクラスのメンバに戻すときはEnum#valueOfとかで戻すんですかね.実装見てないんでわからないのですが.

このEnumDBに入るデータを制限するときに大変便利です.都道府県名を入れたいときに,String型で扱うとしたらバリデーションとか本当に面倒ですから.
都道府県名だったらしばらくは増えたり減ったりしないでしょうし,名前が変わることもないでしょうからDBにはOrdinalなりStringなり好きなのを入れたらいいと思います.

ordinalやname以外の値を扱いたい

さて,ここからが本題です.
私もそのようにしたかったのですが,DBに入れなければならない値が
北海道
青森県
岩手県

という日本語の文字列.どうして日本語の文字列が入るDBになっているのかわからないのですが,そういうDBになっているわけなので,そちらに合わせるしかありませんでした.

一応,Javaは日本語のEnumが扱えます

public enum Prefecture {
    北海道,
    青森県,
    岩手県
    //以下略
}

これだったらEnumType.StringにすればDBに北海道とか入るわけです.

ただ,呼ぶときに Prefecture.北海道 とかしなくちゃいけなくて,キモい上に補完が効かないという点,使いづらいと思います.
さらに,わるいことに,Javaの識別子に使えない文字はもちろん使えないという欠点があります.
このPrefectureの中に「その他(海外)」というのがあって却下(Javaでは識別子に()が使えない).


したがって,
Enumの「Hokkaido」を「北海道」に変換する
というような処理を書かなければなりません.

調べた結果,

  • Enumは普通のクラスと同じで,メソッドやフィールドを持ったりすることができます
  • Enumのメンバ(要素)は宣言したEnumのサブクラスです

4. enum | TECHSCORE(テックスコア)
ということがわかりました.

そんなわけでまずはこんな風にします

public enum Prefecture {
    Hokkaido("北海道");

    private String value;
    private Prefecture(String value) {
        this.value = value;
    }
    public String toString() {
        return this.value();
    }
}

これで,Enumのメンバが日本語の文字を持てるようになりました.もちろん持っているものはStringですので中身はなんでも大丈夫です.Hokkaido.toString()とすれば「北海道」を得られます.

あとは,JPAの設定でEnumType.StringのときにEnum#nameじゃなくてEnum#toStringを呼べば終わりだな…とおもったのですが,そうは問屋が卸しませんでした(・へ・)
あ,Enum#nameはfinalメソッドであるのでオーバーライドできませんorz

しようが無いのでエンティティクラスのほうをこのように書き換えました.

@Basic
String Prefecture;

// Stringで持ってるけど,ユーザーからはPrefecture型でやりとりする
public void setPrefecture(Prefecture pref) {
    this.prefecture = pref.toString();
}

ここからが面倒です.
JPADBの値からEnumの値に変換することもしなければなりません.setterと同じくgetterも定義しなければならないので,文字列->Enumへの変換が必要です.
普通,文字列からEnumといえばはEnum#valueOfです.引数に文字列を渡すと,その名前のEnumが帰ってきます.Prefecture.valueOf("Hokkaido");みたいな.

しかし,今回は"北海道”からHokkaidoを得なければなりません.

対処法

// 文字列->Enumへの逆引きHashMapをつくる
private static HashMap<String, Prefecture> prefList = new HashMap<String, Prefecture>() {{
    for(Prefecture pref : Prefecture.values()) prefList.put(pref.toString(), pref);
}};

// 逆引き辞書を使ってEnumへ変換する
public static Prefecture convertToEnum(String name) {
    return prefList.get(name)
}

これによりgetterを定義できます.

// getするたびにHashMap#getを呼ぶため,何度もgetter呼ぶとパフォーマンス悪い?
public Prefecture getPrefecutere() {
    return Prefecture.convertToEnum(this.prefecture);
}

以上で,Enumに文字列の値をもたせた上でDB<->エンティティクラス間のやりとりが出来るようになりました.


まとめ

余りきれいなやり方とはいえませんが,一応これでEnum型に適当な値を放り込んで運用することができます.
まあ,もちろんConverterを定義して

@Override
public String convertToDataBaseColumn(Prefecture pref) {
    switch(pref) {
        case Prefecture.Hokkaido: return "北海道";
        case Prefecture.Aomori: return "青森県";
        //以下略
    }
}

@Override
public Prefecture convertToEntityAttribute(String pref) {
    if(pref == null) {
        return null;
    } else switch(pref) {
        case "北海道" : return Prefecture.Hokkaido;
        case "青森県": return Prefecture.Aomori;
        //以下略
    }
}

としても良いのですが,同じことを2度も書かなければいけないので,冗長な上,漏れが起きそうで怖いと思います.
Hokkaido("北海道”)と一箇所に書くだけでOKという安心感と,Enumを使えて型的な安心感を得られるという点で良いかと思います.
JPAがtoStringなり好きなメソッドを呼んでくれたらそんなに面倒でもないと思うのですが,そのあたりはやはりConverterにでも言えよというスタンスなのでしょうかね.