S2JDBC で UUID 型を利用する

テーブルの主キーに UUID を使いたい場面は結構あるとおもうのですが、S2JDBC は UUID を利用できるようにはなっていないようです。
ならばってことで、Dialect を拡張して、UUID を使えるようにしてみました。対象の DBMS は H2 です。H2 ならば UUID 型をサポートしているので、そのまま出し入れできます。MySQL 等の場合は、カラムの型を Binary(16) とし、 java.util.UUID のlong値x2をbyte[16] にして格納すればよいかと。

S2JDBC の Dialect を拡張

まずは、S2JDBC の Dialect を拡張。

import java.sql.{CallableStatement, PreparedStatement, ResultSet}
import java.util.UUID
import javax.persistence.TemporalType
import org.seasar.extension.jdbc.{ValueType, PropertyMeta}
import org.seasar.extension.jdbc.dialect.H2Dialect
import org.seasar.extension.jdbc.types.AbstractValueType
import org.seasar.extension.jdbc.util.BindVariableUtil

/**
 * java.util.UUID 型を扱えるようにした H2 の Dialect クラスです。
 */
class UUIDH2Dialect extends H2Dialect {
  import UUIDH2Dialect.{uuidClass, uuidValueType}

  override def getValueType(propertyMeta: PropertyMeta): ValueType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidValueType
    else super.getValueType(propertyMeta)

  override def getValueType(clazz: Class[_], lob: Boolean, temporalType: TemporalType): ValueType =
    if (uuidClass.isAssignableFrom(clazz)) uuidValueType
    else super.getValueType(clazz, lob, temporalType)
}

object UUIDH2Dialect {
  val uuidClass = classOf[UUID]

  /**
   * UUID 型の情報を扱う ValueType クラスです。
   */
  object uuidValueType extends AbstractValueType(java.sql.Types.OTHER) {
    
    def getValue(resultSet: ResultSet, index: Int): AnyRef =
      resultSet.getObject(index).asInstanceOf[UUID]

    def getValue(resultSet: ResultSet, columnName: String): AnyRef =
      resultSet.getObject(columnName).asInstanceOf[UUID]

    def getValue(cs: CallableStatement, index: Int): AnyRef =
      cs.getObject(index).asInstanceOf[UUID]

    def getValue(cs: CallableStatement, parameterName: String): AnyRef =
      cs.getObject(parameterName).asInstanceOf[UUID]

    def bindValue(ps: PreparedStatement, index: Int, value: AnyRef) {
      value match {
        case null => setNull(ps, index)
        case other => ps.setObject(index, value)
      }
    }

    def bindValue(cs: CallableStatement, parameterName: String, value: AnyRef) {
      value match {
        case null => setNull(cs, parameterName)
        case other => cs.setObject(parameterName, value)
      }
    }

    def toText(value: AnyRef): String =
      value match {
        case null => BindVariableUtil.nullText()
        case other => BindVariableUtil.toText(value)
      }
  }
}

拡張した Dialect を使用するように、s2jdbc.dicon を修正。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="jdbc.dicon"/>
	<include path="s2jdbc-internal.dicon"/>

	<!-- UUIDH2Dialect のコンポーネント定義を追加 -->
	<component name="uuidH2Dialect" class="UUIDH2Dialect"/>

	<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>

		<!-- dialect の指定を変更 -->
		<property name="dialect">uuidH2Dialect</property>

	</component>
</components>

S2JDBC-Gen の GenDialect を拡張

また、S2JDBC-Gen も使うので、GenDialect を拡張。

import java.util.UUID
import java.sql.{PreparedStatement, ResultSet, Types}
import org.seasar.extension.jdbc.PropertyMeta
import org.seasar.extension.jdbc.gen.dialect.GenDialect.ColumnType
import org.seasar.extension.jdbc.gen.internal.dialect.H2GenDialect
import org.seasar.extension.jdbc.gen.internal.dialect.StandardGenDialect
import org.seasar.extension.jdbc.gen.internal.sqltype.AbstractSqlType
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider
import org.seasar.extension.jdbc.gen.sqltype.SqlType

class UUIDH2GenDialect extends H2GenDialect {
  import UUIDH2GenDialect.{uuidClass, uuidColumnType, uuidDataType, uuidSqlType}

  override def getSqlType(valueTypeProvider: ValueTypeProvider, propertyMeta: PropertyMeta): SqlType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidSqlType
    else super.getSqlType(valueTypeProvider, propertyMeta)

  override def getColumnType(typeName: String, sqlType: Int): ColumnType =
    if (typeName == uuidDataType) uuidColumnType
    else super.getColumnType(typeName, sqlType)
}

object UUIDH2GenDialect {
  val uuidClass = classOf[UUID]
  val uuidDataType = "uuid"

  object uuidSqlType extends AbstractSqlType(uuidDataType) {
    def getValue(resultSet: ResultSet, index: Int): String =
      resultSet.getObject(index) match {
        case null => null
        case other => other.toString
      }
  
    def bindValue(ps: PreparedStatement, index: Int, value: String) {
      if (value == null) ps.setNull(index, Types.OTHER)
      else ps.setString(index, value)
    }
  }

  object uuidColumnType extends StandardGenDialect.StandardColumnType(uuidDataType, uuidClass)
}

S2JDBC-Gen の各タスクで、拡張した Dialect を使用するようにビルドファイルを修正。

<project name="example-s2jdbc-gen" default="gen-ddl" basedir=".">
  
  〜(省略)〜

  <property name="gendialectclassname" value="UUIDH2GenDialect"/>

  <target name="gen-ddl">
    <!-- gendialectclassname 属性を指定する -->
    <gen-ddl
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      gendialectclassname="${gendialectclassname}" 
    />

    〜(省略)〜
  </target>

  〜(省略)〜

  <target name="migrate">
    <!-- gendialectclassname 属性を指定する -->
    <migrate
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      applyenvtoversion="${applyenvtoversion}"
      version="${version}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      gendialectclassname="${gendialectclassname}" 
    />
    <refresh projectName="${projectname}"/>
  </target>

  〜(省略)〜

</project>

以上により、エンティティのプロパティ型として java.util.UUID が使えました。ただ、@GeneratedValue までは対応しなかったので、insert の前に、自分で UUID を設定しておく必要があります。

おまけ

ほかの DB 用の Dialect を作ることも想定して、共通部分をトレイトとして分離してみる。

UUID 対応 Dialect のトレイト

import java.util.UUID
import javax.persistence.TemporalType
import org.seasar.extension.jdbc.{DbmsDialect, PropertyMeta, ValueType}

trait UUIDDialect extends DbmsDialect {
  import UUIDDialect.uuidClass

  protected def uuidValueType: ValueType

  abstract override def getValueType(propertyMeta: PropertyMeta): ValueType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidValueType
    else super.getValueType(propertyMeta)

  abstract override def getValueType(clazz: Class[_], lob: Boolean, temporalType: TemporalType): ValueType =
    if (uuidClass.isAssignableFrom(clazz)) uuidValueType
    else super.getValueType(clazz, lob, temporalType)
}

object UUIDDialect {
  val uuidClass = classOf[UUID]
}

UUID 対応 GenDialect のトレイト

import java.util.UUID
import java.sql.{PreparedStatement, ResultSet}

import org.seasar.extension.jdbc.{ValueType, PropertyMeta}
import org.seasar.extension.jdbc.gen.dialect.GenDialect
import org.seasar.extension.jdbc.gen.internal.dialect.StandardGenDialect
import org.seasar.extension.jdbc.gen.internal.sqltype.AbstractSqlType
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider
import org.seasar.extension.jdbc.gen.sqltype.SqlType

trait UUIDGenDialect extends GenDialect {
  import UUIDDialect.uuidClass
  import UUIDGenDialect.{UUIDColumnType, UUIDSqlType}

  protected def uuidDataType: String
  protected def uuidValueType: ValueType

  private val uuidSqlType = new UUIDSqlType(uuidDataType, uuidValueType)
  private val uuidColumnType = new UUIDColumnType(uuidDataType)

  abstract override def getSqlType(valueTypeProvider: ValueTypeProvider, propertyMeta: PropertyMeta): SqlType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidSqlType
    else super.getSqlType(valueTypeProvider, propertyMeta)

  abstract override def getColumnType(typeName: String, sqlType: Int): GenDialect.ColumnType =
    if (typeName == dataType) uuidColumnType
    else super.getColumnType(typeName, sqlType)
}

object UUIDGenDialect {
  import UUIDDialect.uuidClass
  
  class UUIDSqlType(dataType: String, valueType: ValueType) extends AbstractSqlType(dataType) {
    def getValue(resultSet: ResultSet, index: Int): String =
      valueType.getValue(resultSet, index) match {
        case null => null
        case uuid: UUID => uuid.toString
      }

    def bindValue(ps: PreparedStatement, index: Int, value: String) {
      value match {
        case null => valueType.bindValue(ps, index, null)
        case other => valueType.bindValue(ps, index, UUID.fromString(value)) 
      }
    }
  }

  class UUIDColumnType(dataType: String) extends StandardGenDialect.StandardColumnType(dataType, uuidClass)
}

使用例は以下

class UUIDH2Dialect extends H2Dialect with UUIDDialect {
  protected val uuidValueType = UUIDH2Dialect.uuidValueType
}

class UUIDH2GenDialect extends H2GenDialect with UUIDGenDialect {
  protected val uuidDataType = "uuid"
  protected val uuidValueType = UUIDH2Dialect.uuidValueType
}

// UUIDH2Dialect.uuidValueType は上述のものと同様

いじょ。