S2JDBCのEnum型の変換でname()やordinal()以外の任意の値を利用したい


2010年 12月 01日

JPAの@Enumeratedに渡せるEnumTypeって、なんでSTRINGとORDINALしかないんでしょうか。

JPAではSTRINGを指定した場合はname()メソッドの結果を、ORDINALを指定した場合ははordinal()メソッドの結果をDBに保存するようになっています。(といってもS2JDBCの知識ですが。)
このルールはJavaの言語仕様からするととても明確で、重複を許さなく、一意の変換が行えるという利点があります。

でも、そのきれいな設計には悲しい欠点が。たとえば次の様な時刻の区分値とか。

10:00
14:00
18:00

もしくは飛び飛びの数字とか

-5
-1
0
1
5

数字から始まったり大文字じゃなかったりとか

empty
1st
2nd

こんなコード体系の場合もSTRINGやORDINAL使わないといけないのでしょうか?現実はもっと残酷だよ。。。。

というわけで、S2JDBCで任意の文字列、数字を扱える様なクラスを書いてみました。

Javaの値をDBの値に変換するクラスを用意する

S2JDBCではorg.seasar.extension.jdbc.types.ValueTypes(のstaticブロック)でJavaの型とDBの型の変換を定義しています。

265         try {
266             isEnumMethod = Class.class.getMethod("isEnum", null);
267             setEnumDefaultValueType(Class
268                     .forName("org.seasar.extension.jdbc.types.EnumOrdinalType"));
269             setEnumOrdinalValueType(Class
270                     .forName("org.seasar.extension.jdbc.types.EnumOrdinalType"));
271             setEnumStringValueType(Class
272                     .forName("org.seasar.extension.jdbc.types.EnumType"));
273         } catch (Throwable ignore) {
274             isEnumMethod = null;
275             enumStringValueTypeConstructor = null;
276             enumOrdinalValueTypeConstructor = null;
277         }

このクラスを差し替えれば任意の変換が行えます。

ここでは任意のStringをDBに保存できるEnumTypeを定義します。(Integerの例はgithubを参照してください。)

カスタムの値を利用するEnumが実装してほしいインターフェースを定義します。インターフェースでは明示できませんが、StringからEnumを作るためのファクトリーメソッドのを呼び出すHelperも定義しておきます。

package jp.troter.seasar.extension.jdbc.types;

import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.MethodNotFoundRuntimeException;
import org.seasar.framework.beans.factory.BeanDescFactory;

public interface StringCode {

    public static final String FACTORY_METHOD_NAME = "codeOf";
    public static final String PROPERTY_CODE = "code";

    /**
     * @return コード
     */
    String getCode();

    /**
     * ヘルパー
     */
    public static class Helper {

        public static void assertFactoryMethod(final Class<? extends Enum> enumClass) {
            try {
                BeanDesc b = BeanDescFactory.getBeanDesc(enumClass);
                b.getMethod(FACTORY_METHOD_NAME, new Class[]{String.class});
            } catch (MethodNotFoundRuntimeException e) {
                String m = String.format("[%s]を継承したクラス[%s]はスタティックメソッド[%s]を実装する必要があります。", StringCode.class.getName(), enumClass.getName(), FACTORY_METHOD_NAME);
                throw new IllegalArgumentException(m, e);
            }
        }

        public static Enum codeOf(final Class<? extends Enum> enumClass, String code) {
            BeanDesc b = BeanDescFactory.getBeanDesc(enumClass);
            return (Enum)b.invoke(null, FACTORY_METHOD_NAME, new Object[]{code});
        }

        public static String getCode(final Class<? extends Enum> enumClass, Object value) {
            BeanDesc b = BeanDescFactory.getBeanDesc(enumClass);
            return (String)b.getPropertyDesc(PROPERTY_CODE).getValue(value);
        }
    }
}

次に、このインターフェースを使うEnumTypeを定義します。EnumTypeをベースに書き換えています。toEnumメソッド、fromEnumメソッドが改造している部分です。

package jp.troter.seasar.extension.jdbc.types;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

import org.seasar.extension.jdbc.types.AbstractValueType;
import org.seasar.extension.jdbc.util.BindVariableUtil;

public class EnumStringType extends AbstractValueType {

    @SuppressWarnings("unchecked")
    private final Class<? extends Enum> enumClass;

    /**
     * インスタンスを構築します。
     * 
     * @param enumClass
     */
    @SuppressWarnings("unchecked")
    public EnumStringType(Class<? extends Enum> enumClass) {
        super(Types.VARCHAR);
        this.enumClass = enumClass;
        if (StringCode.class.isAssignableFrom(enumClass)) {
            StringCode.Helper.assertFactoryMethod(enumClass);
        }
    }

    @Override
    public Object getValue(ResultSet resultSet, int index) throws SQLException {
        return toEnum(resultSet.getString(index));
    }

    /**
     * {@link Enum}に変換します。
     * 
     * @param name
     * @return {@link Enum}
     */
    @SuppressWarnings("unchecked")
    protected Enum toEnum(String name) {
        if (name == null) {
            return null;
        }
        if (StringCode.class.isAssignableFrom(enumClass)) {
            return StringCode.Helper.codeOf(enumClass, name);
        }
        return Enum.valueOf(enumClass, name);
    }

    @Override
    public Object getValue(ResultSet resultSet, String columnName)
            throws SQLException {

        return toEnum(resultSet.getString(columnName));
    }

    @Override
    public Object getValue(CallableStatement cs, int index) throws SQLException {
        return toEnum(cs.getString(index));
    }

    @Override
    public Object getValue(CallableStatement cs, String parameterName)
            throws SQLException {

        return toEnum(cs.getString(parameterName));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void bindValue(PreparedStatement ps, int index, Object value)
            throws SQLException {

        if (value == null) {
            setNull(ps, index);
        } else {
            ps.setString(index, fromEnum(value));
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void bindValue(CallableStatement cs, String parameterName,
            Object value) throws SQLException {

        if (value == null) {
            setNull(cs, parameterName);
        } else{
            cs.setString(parameterName, fromEnum(value));
        }
    }

    @Override
    public String toText(Object value) {
        if (value == null) {
            return BindVariableUtil.nullText();
        }
        return BindVariableUtil.toText(fromEnum(value));
    }

    /**
     * {@link Enum}を文字列表現に変換します。
     * @param value {@link Enum}
     * @return 文字列
     */
    protected String fromEnum(Object value) {
        if (StringCode.class.isAssignableFrom(enumClass)) {
            return StringCode.Helper.getCode(enumClass, value);
        }
        return (Enum.class.cast(value)).name();
    }

}

定義したValueTypeを使ってみる。

使うときは、s2jdbc.diconで次の様な設定をしてEnumTypeを差し替えます。

  <component name="jdbcManager"
   class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
    <property name="maxRows">0</property>
    <property name="fetchSize">0</property>
    <property name="queryTimeout">0</property>
    <property name="dialect">h2Dialect</property>
    <property name="allowVariableSqlForBatchUpdate">true</property>
    <!--
    <initMethod>
      @org.seasar.extension.jdbc.types.ValueTypes@setEnumDefaultValueType(
        @jp.troter.seasar.extension.jdbc.types.EnumIntegerType@class)
    </initMethod>
    <initMethod>
      @org.seasar.extension.jdbc.types.ValueTypes@setEnumOrdinalValueType(
        @jp.troter.seasar.extension.jdbc.types.EnumIntegerType@class)
    </initMethod>
    -->
    <initMethod>
      @org.seasar.extension.jdbc.types.ValueTypes@setEnumStringValueType(
        @jp.troter.seasar.extension.jdbc.types.EnumStringType@class)
    </initMethod>
  </component>

カスタムの値を使いたいEnumを定義します。

package jp.troter.sample.enums;

import jp.troter.seasar.extension.jdbc.types.StringCode;

public enum ImplementedStringCode implements StringCode {
   EMPTY("empty")
   FIRST("1st"),
   SECOND("2nd"),
   ;

   private final String code;

   ImplementedStringCode(String code) {
       this.code = code;
   }

   @Override
   public String getCode() {
       return code;
   }

   public static ImplementedStringCode codeOf(String code) {
       for (ImplementedStringCode e : values()) {
           if (e.getCode().equals(code)) {
               return e;
           }
       }
       return null;
   }
}

あとはいつものS2JDBCのEntityの定義で利用するだけです。

@Entity
public class Sample {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    @Enumerated(EnumType.STRING)
    public ImplementedStringCode stringCode;

    @Override
    public String toString() {
        StringBuilder s = new StringBuilder();
        s.append(this.getClass().getSimpleName()).append("[");
        s.append("id=").append(id).append(", ");
        s.append("stringCode=").append(stringCode).append("]");
        return s.toString();
    }
}

githubにサンプルコードも含めて置いてあります
EnumIntegerTypeという任意の整数を扱えるものも置いてありますが、EnumNumericTypeの様なクラスに改造して浮動小数点などが扱える様にすると楽しそうです。複雑な世界で困っている人は使ってみてください。