Enum と データベースの「コード値」の相互変換
データベースで「コード値」的なものを使うことはよくある。たとえば、以下のような顧客データベースがあって、「ランク」は 1 が通常、2 がVIPを意味する、だとか。
ID | 名前 | ランク |
---|---|---|
101 | 山田 奈緒子 | 1 |
102 | 上田 次郎 | 2 |
Javaにはこういうのを表現するのにぴったりな 列挙型 という仕組みがあり、たとえば上の「ランク」は以下のように表現できる。
CustomerRank.java
public enum CustomerRank { NORMAL(1, "通常"), VIP(2, "VIP"), ; private final Integer code; private final String name; CustomerRank(Integer code, String name) { this.code = code; this.name = name; } public Integer getCode() { return code; } public String getName() { return name; } }
これで、アプリケーションコード上で 1 とか 2 とかのコード値を直接触らなくても良くなるし、コードに属する振る舞いを定義しやすくなるし(例えば、割引ロジック DiscountPolicy を作成して、上の VIP と NORMAL に割り当てるとか)、とてもわかりやすくなる。
ただ、このままだとデータベースから読んできたコード値を enum に直すのが面倒なので、以下のような汎用的な仕組みを作る。
Encodable.java
import java.io.Serializable; public interface Encodable<T extends Serializable> { T encode(); }
Decoder.java
import java.io.Serializable; import java.util.HashMap; import java.util.Map; public class Decoder<K extends Serializable, V extends Encodable<K>> { private Map<K, V> map; private Decoder(V[] values) { map = new HashMap<K, V>(values.length); for (V value : values) { V old = map.put(value.encode(), value); // コード値の重複はサポートしない if (old != null) { throw new IllegalArgumentException("duplicated code: " + value); } } } public V decode(K code) { return map.get(code); } // 型引数の指定を省略するため public static <K1 extends Serializable, V1 extends Encodable<K1>> Decoder<K1, V1> create(V1[] values) { return new Decoder<K1, V1>(values); } }
改造版の CustomerRank.java
public enum CustomerRank implements Encodable<Integer> { NORMAL(1, "通常"), VIP(2, "VIP"), ; private final Integer code; private final String name; private static final Decoder<Integer, CustomerRank> decoder = Decoder.create(values()); CustomerRank(Integer code, String name) { this.code = code; this.name = name; } public static CustomerRank decode(Integer code) { return decoder.decode(code); } @Override public Integer encode() { return code; } public String getName() { return name; } }
このような仕組みを用意しておくと、以下のように簡単に変換できるようになるし、
// コード値 ⇒ enum CustomerRank rank = CustomerRank.decode(1); // enum ⇒ コード値 Ingeger code = rank.encode();
他の enum にも最低限の労力で同様の encode/decode メカニズムを追加できる。
public enum Prefecture implements Encodable<String> { HOKKAIDO("01", "北海道"), ... OKINAWA("47", "沖縄"), ; private final String code; private final String name; private final Decoder<String, Prefecture> decoder = Decoder.create(values()); Prefecture(String code, String name) { this.code = code; this.name = name; } public static Prefecture decode(String code) { return decoder.decode(code); } @Override public String encode() { return code; } }
enum の name() 値(CustomerRank での "NORMAL" や "VIP")をそのまま永続化するよりも、このように永続化用の内部コード値を別に持ったほうが良いと思う。DB上の表現をアプリ開発者が完全に制御できるとは限らないし、変数名を変えられないのは不自由だし。
また、上のように汎用的な変換の仕組みを作っておけば、ORマッパに自動変換させることもでき、さらに楽だ。例えば Hibernate の場合*1、 http://pastebin.com/f39d77565 にあるようなコードを書き、hbmファイルに
<class name="example.Customer" table="CUSTOMERS"> ... <property name="rank"> <type name="at.molindo.util.hibernate.EnumUserType"> <param name="enumClass">example.CustomerRank</param> <param name="identifierMethod">encode</param> <param name="valueOfMethod">decode</param> </type> </property>
のように定義しておけば、DBと読み書きする時に勝手に相互変換してくれて、アプリケーション側では完全に enum のみで「コード」を扱うことができる。
*1:4年くらい触ってないので知識が古い可能性あり。今はもっと簡単にできたりするかも?