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 が例外を吐いて失敗することがあります*3。S2JDBC や 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 を問題なく扱えました。Scala も S2JDBC も便利です :-)
*1:トレイトの実装部分は、クラスのコードとして埋め込まれるためと推測されます
*2:Scala は 一応 .NET でも使用できるように意図していたためか、java.io.Serializable のような Java 固有なインタフェースを使う代わりに、@serializable のようなアノテーションを使用するということになっています。.NET 用のコンパイラでは、生成されるクラスに [serializable] 等の属性がつくようになるのだと思います
*3:再現する条件は調べきれていません、型パラメータと組み合わせたときに発生している気がします
*4:深く調査しきれていないため勘違いしている箇所もあるかもしれません :-p