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で任意の文字列、数字を扱える様なクラスを書いてみました。
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();
}
}
使うときは、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の様なクラスに改造して浮動小数点などが扱える様にすると楽しそうです。複雑な世界で困っている人は使ってみてください。