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 の場合*1http://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年くらい触ってないので知識が古い可能性あり。今はもっと簡単にできたりするかも?