2014年7月10日木曜日

JDO Enum Mapping

  Java Data Objects 3.0 (JSR 243) の仕様によると、Enum の値をストレージに永続化する際、ストレージでの型が数値型であれば Enum.ordinal() メソッドの返す値が、文字列型であれば Enum.name() メソッドの返す値が用いられるとのこと。 (15.1 Column Elements / Mapping enums)

  Enum にマッピングされるカラムのデフォルトの jdbc-type は VARCHAR なので、ordinal() の値を使いたいときは、当該カラムに対して明示的に jdbc-type を指定したほうがよい。有効な jdbc-type は次のとおりで、全て大文字か全て小文字かのどちらかで指定する。 (18.4 ELEMENT column)

CHAR, VARCHAR, LONGVARCHAR, NUMERIC, DECIMAL, BIT, TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP

  DataNucleus は、name()ordinal() ではない別の値で Enum を永続化する拡張を提供している。下記は、「JDO : Persistable Field Types」 に挙げられている例である。

public enum MyColour 
{
    RED((short)1), GREEN((short)3), BLUE((short)5), YELLOW((short)8);

    private short value;

    private MyColour(short value)
    {
        this.value = value;
    }

    public short getValue() 
    {
        return value;
    }

    public static MyColour getEnumByValue(short value)
    {
        switch (value)
        {
            case 1:
                return RED;
            case 3:
                return GREEN;
            case 5:
                return BLUE;
            default:
                return YELLOW;
        }
    }
}
@Extensions({
    @Extension(vendorName="datanucleus",
               key="enum-getter-by-value", value="getEnumByValue"),
    @Extension(vendorName="datanucleus",
               key="enum-value-getter", value="getValue")
   })
MyColour colour;

  name() で永続化する場合は 「"RED", "GREEN", "BLUE", "YELLOW"」、 ordinal() で永続化する場合は 「0, 1, 2, 3」 であるが、上記の例のようにすれば、「1, 3, 5, 8」 という値で永続化される。

  ただ、ここに誤りやすいポイントがある。DataNucleus の拡張を使う場合、値のデータ型は short でなければならないようだ。例えば上記の実装で getEnumByValue(short value) となっているところを getEnumByValue(int value) に変更すると、拡張機能が動作しない。逆にデータサイズを小さくする getEnumByValue(byte value) でも、やはり動作しない。ただ、ストレージでの型は必ずしも SMALLINT でなくてもよいようだ。

  例えば、ClientType という enum を定義し、PUBLICCONFIDENTIAL というエントリーを持たせ、それぞれ 1, 2 という値で永続化したい場合、その実装は次のようになる。


public enum ClientType 
{
    PUBLIC((short)1),
    CONFIDENTIAL((short)2); 
 
    private static final ClientType[] mValues = values(); 
    private final short mValue;
 
    private ClientType(short value)
    {
        mValue = value;
    }
 
    public short getValue()
    {
        return mValue;
    }
 
    public static ClientType getByValue(short value)
    {
        if (value < 1 || mValues.length < value)
        {
            return null;
        }

        return mValues[value - 1];
    }
}

  この ClientType 型のフィールドを持つ ClientEntity クラスを定義するとすれば次のような感じになる。


@PersistenceCapable(
        identityType = IdentityType.APPLICATION,
        table        = "client",
        detachable   = "true")
public class ClientEntity 
{
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    @Column(name = "number")
    private int number;
 
    @Persistent(defaultFetchGroup = "true")
    @Column(name = "client_type", jdbcType = "TINYINT")
    @Extensions({
        @Extension(vendorName = "datanucleus",
                   key = "enum-getter-by-value", value = "getByValue"),
        @Extension(vendorName = "datanucleus",
                   key = "enum-value-getter", value = "getValue")
    })
    private ClientType clientType;

    public int getNumber()
    {
        return number;
    }

    public ClientType getClientType()
    {
        return clientType;
    }

    public void setClientType(ClientType clientType)
    {
        this.clientType = clientType;
    }
}

ClientEntity クラスに対応するテーブル "client" を MySQL で定義するならば、次のような感じ。

CREATE TABLE client
(
    number INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    client_type TINYINT UNSIGNED NOT NULL DEFAULT 1
);