Scala から S2JDBC を利用するときに気をつけること

Scala から S2JDBC を利用するときに気付いたこととかを、いくつか書いておきます。

タイプセーフ API

S2JDBC では Operations クラスや S2JDBC-Gen により生成した Names クラスを、以下のように利用することで型や名前の間違いをコンパイル時に気付けるようになっています。

// インポート
import static org.seasar.extension.jdbc.operation.Operations.*;
import static sample.entity.FooNames.*;
// eq は Operations, name() は FooNames のメソッド
public List<Foo> findByName(String name) {
  return jdbcManager.from(Foo.class)
                    .where(eq(name(), name))
                    .getResultList();
}

Scala の場合でも Java と同じように Operations クラスと Names クラスをスタティックインポートして使用すると、残念なことにコンパイルに失敗します。
以下はコンパイルに失敗する例です。

// インポート
import scala.collection.jcl.Conversions._ // java.util.List<Foo> → Seq[Foo]
import org.seasar.extension.jdbc.operation.Operations._
import sample.entity.FooNames._
// eq は Operations, name() は FooNames のメソッド
def findByName(name: String): Seq[Foo] =
  jdbcManager.from(classOf[Foo])
             .where(eq(name(), name))
             .getResultList;

Scala の場合は AnyRef クラスで eq メソッドが定義されているため、eq(...) を呼び出すと this の eq が呼び出されてしまいます。また、メソッドと変数は同じ名前空間を使用するため、name() は FooNames.name() ではなくメソッド引数の name だと解釈されてしまいます。

このため、Scala からタイプセーフAPIを利用する場合は、インポート時に別名をつけるかスタティックインポートを使用しない等の方法をとる必要があります。私の場合、Operations クラスと Names クラスに別名をつけて使用しています。

// インポート
import scala.collection.jcl.Conversions._
import org.seasar.extension.jdbc.operation.{Operations=>op}
import sample.entity.{FooNames=>ns}
// op.eq は Operations, ns.name は FooNames のメソッド
def findByName(name: String): Seq[Foo] =
  jdbcManager.from(classOf[Foo])
             .where(op.eq(ns.name, name))
             .getResultList;

エンティティのフィールド

S2JDBC/S2JDBC-Gen ではエンティティのパブリックフィールドに対して、テーブルのカラムをマッピングできるため、@BeanInfo や @BeanProperty をつける必要はありません。Scala でエンティティを書く場合は var をつかってカラムに対応するフィールドを定義します。また、アノテーションにパラメータを指定するには以下のように { } 内に記述します。

import javax.persistence.{Column, GeneratedValue, Entity, Id}

@Entity
class Foo {
  @Id
  @GeneratedValue
  var id: Int = _

  @Column{val nullable = false, val unique = true}
  var name: String = _
}

トレイトの使用

振る舞いや状態を持つトレイトであっても、問題なく使用できるようです。

import javax.persistence.{Column, GeneratedValue, Entity, Id}

trait Base {
  @Id
  @GeneratedValue
  var id: Int = _
}  

trait Named {
  @Column{val nullable = false, val unique = true}
  var name: String = _
}

@Entity
class Foo extends Base with Named

トレイトに @MappedSuperclass を指定する必要は無いようです。*1
Javaの場合、共通のカラムなどは @MappedSuperclass をつけた親クラスにまとめるしかないため、この辺りは Scala の利点が良く出ているかと思います。

serializable や cloneable を指定する場合

Scala で対象のクラスを直列化可能とするには @serializable アノテーションを、複製を可能とするには @cloneable アノテーションを指定します。*2
しかし、java で class Foo implements java.io.Serializable とした時と生成されるクラスが微妙に異なるらしく、S2JDBC-Gen が例外を吐いて失敗することがあります*3S2JDBC や javax.persistence.* のアノテーションを利用している時点で、.NET で使えるクラスを出力するつもりは無いため、ここは java と同じように java.io.Serializable や java.lang.Cloneable を指定しておきます。

trait Base extends java.io.Serializable {
  ...
}  

型パラメータを使用する場合

関連クラスの型に型パラメータを使用すると、S2JDBC-Gen の時点で失敗します。例えば以下のような場合です。

trait Head[A<:Head[A, B], B<:Detail[B, A]] extends Base { self: A =>
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[B] = _
}

trait Detail[A<:Detail[A, B], B<:Head[B, A]] extends Base { self: A =>
  @Column
  var headId: Int = _
  @ManyToOne
  var head: B = _
}

@Entity
class Foo extends Head[Foo, Bar] with Named

@Entity
class Bar extends Detail[Bar, Foo] with Named

これはおそらく Java でも同様だと思うのですが、Head や Detail に対しては型パラメータの上限境界までしか型情報を取れず、それらの型は Entity ではないため失敗します。型パラメータを使用する場合は、トレイトやクラスでは抽象メンバにして、実装クラスで実際の型を指定したフィールドを作ったほうが良いようです。

trait Head[A<:Head[A, B], B<:Detail[B, A]] extends Base { self: A =>
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[B] // 抽象メンバ
}

trait Detail[A<:Detail[A, B], B<:Head[B, A]] extends Base { self: A =>
  @Column
  var headId: Int = _
  @ManyToOne
  var head: B // 抽象メンバ
}

@Entity
class Foo extends Head[Foo, Bar] with Named {
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[Bar] = _ // 具象型を指定
}

@Entity
class Bar extends Detail[Bar, Foo] with Named {
  @ManyToOne
  var head: Foo = _ // 具象型を指定
}

エンティティ取得結果

S2JDBC では getResultList で 0 件以上のエンティティを保持した java.util.List[T] が、getSingleResult で null か エンティティインスタンスが取得されます。これらの取得結果を Scala らしく扱えるように、java.util.List[T] は Seq[T] に、null かエンティティインスタンスは Option に変換します。

class FooService(jdbcManager: JdbcManager) {
  /** java.util.List[T] → Seq[T] への変換 */
  import scala.collection.jcl.Conversions._ 

  /** null/エンティティ → Option への変換 */
  private implicit def entityToOption(entity: Foo): Option[Foo] =
    entity match {
      case null => None
      case _ => Some(entity)
    }

  def findAll: Seq[Foo] =
    jdbcManager.from(classOf[Foo])
               .getResultList

  def findById(id: Int): Option[Foo] =
    jdbcManager.from(classOf[Foo])
               .id(id.asInstanceOf[java.lang.Integer])
               .getSingleResult
}

ついでに共通部分を抽象クラスにまとめてしまいます。

/**
 * エンティティを操作するサービスの抽象親クラス
 */
abstract class AbstractService[T](jdbcManager: JdbcManager)
                                 (implicit manifest: scala.reflect.Manifest[T]) {

  protected val entityClass: Class[T] = manifest.erasure.asInstanceOf[Class[T]]
  protected def select: AutoSelect[T] = jdbcManager.from(entityClass)

  /** java.util.List[T] → Seq[T] への変換 */
  import scala.collection.jcl.Conversions._

  /** null/エンティティ → Option への変換 */
  protected implicit def entityToOption(entity: T): Option[T] =
    entity match {
      case null => None
      case _ => Some(entity)
    }

  def findAll: Seq[T] =
    select.getResultList

  def findById(id: Int): Option[T] =
    select.id(id.asInstanceOf[java.lang.Integer])
          .getSingleResult

  def insert(entity: T): Int =
    jdbcManager.insert(entity).execute

  def update(entity: T): Int =
    jdbcManager.update(entity).execute

  def delete(entity: T): Int =
    jdbcManager.delete(entity).execute
}

/** 
 * Foo エンティティを操作するサービス */
 */
class FooService(jdbcManager: JdbcManager) extends AbstractService[Foo](jdbcManager) {
  import scala.s2jdbc.entity.{FooNames => ns}
  override protected def select =
    super.select.leftOuterJoin(ns.details)
}

/**
 * Bar エンティティを操作するサービス
 */
class BarService(jdbcManager: JdbcManager) extends AbstractService[Bar](jdbcManager)

以上をふまえることで、Scala からも S2JDBC を問題なく扱えました。ScalaS2JDBC も便利です :-)

*4

*1:トレイトの実装部分は、クラスのコードとして埋め込まれるためと推測されます

*2:Scala は 一応 .NET でも使用できるように意図していたためか、java.io.Serializable のような Java 固有なインタフェースを使う代わりに、@serializable のようなアノテーションを使用するということになっています。.NET 用のコンパイラでは、生成されるクラスに [serializable] 等の属性がつくようになるのだと思います

*3:再現する条件は調べきれていません、型パラメータと組み合わせたときに発生している気がします

*4:深く調査しきれていないため勘違いしている箇所もあるかもしれません :-p