JPAで値付きのEnumを扱う
JPA実装によって話は変わってくるかもしれません.わたしはEclipseLinkを使っています.
JPAでEnumを扱うとき
@Enumerated(EnumType.String) private HogeEnum hoge;
ふつうはこうすると思います.
@Enumeratedアノテーションを列挙型の変数につけると,JPAがpersistしてDBにinsertするときに,EnumTypeの値にしたがって変換してくれます.
Stringを指定すると,Enum#name,Ordinalを指定すると,Enumを指定するとEnum#ordinalをそれぞれ呼んだ結果に変換されて永続化されます.
DBの値からエンティティクラスのメンバに戻すときはEnum#valueOfとかで戻すんですかね.実装見てないんでわからないのですが.
このEnum.DBに入るデータを制限するときに大変便利です.都道府県名を入れたいときに,String型で扱うとしたらバリデーションとか本当に面倒ですから.
都道府県名だったらしばらくは増えたり減ったりしないでしょうし,名前が変わることもないでしょうからDBにはOrdinalなりStringなり好きなのを入れたらいいと思います.
ordinalやname以外の値を扱いたい
さて,ここからが本題です.
私もそのようにしたかったのですが,DBに入れなければならない値が
北海道
青森県
岩手県
…
という日本語の文字列.どうして日本語の文字列が入るDBになっているのかわからないのですが,そういうDBになっているわけなので,そちらに合わせるしかありませんでした.
public enum Prefecture { 北海道, 青森県, 岩手県 //以下略 }
これだったらEnumType.StringにすればDBに北海道とか入るわけです.
ただ,呼ぶときに Prefecture.北海道 とかしなくちゃいけなくて,キモい上に補完が効かないという点,使いづらいと思います.
さらに,わるいことに,Javaの識別子に使えない文字はもちろん使えないという欠点があります.
このPrefectureの中に「その他(海外)」というのがあって却下(Javaでは識別子に()が使えない).
したがって,
Enumの「Hokkaido」を「北海道」に変換する
というような処理を書かなければなりません.
調べた結果,
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(); }
ここからが面倒です.
JPAがDBの値から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にでも言えよというスタンスなのでしょうかね.
JPAのConverterで独自クラスを扱う
ConverterをOpenJPAで使おうとしましたが,今のOpenJPAのリリースではConverterに対応していませんでした.
ConverterはJPA2.1以降の実装にしか対応していません.HibernateかEclipseLinkでやりましょう.
私はEclipselinkです
さてコンバーターの説明はこちらでご覧ください.
JavaEE 7 JPA 2.1の新機能コンバータ - しんさんの出張所 はてな編
JPA2.1 の Converter を enum + コードファーストで試す - hd 4.0
上記2記事にあるように,JPAでDBに入っているデータと実際にJava上で扱いたいデータ型が異なるときにコンバーターは非常に便利です.
public Hoge{ private Integer fuga; private Integer piyo; // setter, getter省略 public Hoge(String dododo) { if(dododo.length != 2) throw new IllegalArgumentExeption(); this.fuga = //省略 } @Override public String toString() { return this.fuga.toString() + this.piyo.toString(); } }
@Converter public HogeConverter implements AttributeConverter<Hoge, String>{ @Override public String convertToDatabaseAttribute(Hoge hoge) { return hoge.toString(); } @Override public Hoge convertToEntityAttribute(String value) { return new Hoge(value); } }
例えばこんなのを作ってあとはエンティティクラスのご所望のフィールドに@Convert(converter = "name")とするだけで,出し入れの時にConverterを適用してくれるのです.
ところが
Entityクラスのインスタンスに対して
getHoge().setPiyo(2);
とかした後にEntityManager#persistをしてみても,どうもうまくいきません.
newしたオブジェクトだったらpersistすると更新されるようですが,インスタンスの中身を書き換えただけではJPAには変更されたと認識されないようです.
いろいろ調べた結果
Entityクラスの定義で
@Basic @Mutable // <- 重要 private Hoge hoge;
としなければならなかったようです.
@Mutable | EclipseLink 2.4.x Java Persistence API (JPA) Extensions Reference
毎回インスタンスを生成する必要があるものについてはそんなに気にしなくていいのですが,中身が書き換わるMutableな独自クラスを定義する場合はご注意ください.
それにしてもJPAは日本語ドキュメントが少なくて大変だ.