Locked History Actions

Play/AnormMultiple

anormで複数DBへのアクセス

自前のjava.sql.Connection

play frameworkは、基本的にconf/application.confに記述されたデータベースに対して自動的なアクセスを提供するようだが、これでは「あらかじめ決められたただ一つのDB」へのアクセスしかできない。 playの将来バージョンでは、この記述が複数認められるようになる模様であるが、しかし実行時にDBを自由に切り替えるというわけにはいかないようだ。

しかし、Anorm.scalaのソースを読んでみるとわかるのだが、実はアプリ側で任意のjava.sql.Connectionを用意してやりさえすれば、そこにアクセスすることは簡単にできる。例えば、

val clients = SQL("select * from clients order by id")().map(row => .....

と記述すると、デフォルトのデータベースに対するアクセスになるが、これに自前のjava.sql.Connectionを指定することにより、

val clients = SQL("select * from clients order by id")(connection).map(row => .....

そのデータベースへのアクセスとなる。。。。

。。。はずだが、実際にはならない。これはanormのコードのバグのため(play-scala 0.9.1時点)。 play.db.anorm.Sqlトレイトのソースは以下のようになっているが、

    trait Sql {

        import SqlParser._
        import scala.util.control.Exception._

        def connection = play.db.DB.getConnection

        def getFilledStatement(connection:java.sql.Connection, getGeneratedKeys:Boolean=false):java.sql.PreparedStatement

        def filledStatement = getFilledStatement(connection)

        def apply(conn:java.sql.Connection=connection) = Sql.resultSetToStream(resultSet(connection))

        def resultSet (conn:java.sql.Connection=connection) = (getFilledStatement(connection).executeQuery())

例えば、

 def apply(conn:java.sql.Connection=connection) = Sql.resultSetToStream(resultSet(connection))

 def apply(conn:java.sql.Connection=connection) = Sql.resultSetToStream(resultSet(conn))

でなければいけない。2.0-previewでは修正されているのだろうか???

デフォルトデータベースの設定は必要

ただし、現在のplay-scala 0.9.1では、自前のjava.sql.Connectionを使う場合にも「デフォルトデータベース」の設定は必要になる。 設定せずに、上に述べたアクセスを使用とすると、以下のエラーが発生する。

Exception in thread "main" java.lang.NullPointerException
        at play.exceptions.PlayException.getInterestingStrackTraceElement(PlayException.java:56)
        at play.exceptions.DatabaseException.<init>(DatabaseException.java:24)
        at play.db.DB.getConnection(DB.java:63)
        at play.db.anorm.Sql$class.connection(Anorm.scala:899)
        at play.db.anorm.SqlQuery.connection(Anorm.scala:938)
        at play.db.anorm.Sql$class.apply(Anorm.scala:905)
        at play.db.anorm.SqlQuery.apply(Anorm.scala:938)
        at Main$.main(Main.scala:30)
        at Main.main(Main.scala)

この理由はソースを見ればわかる。先のSQL()メソッド呼び出しはplay.db.anorm.Sqlというトレイトを返すのだが、このトレイトの定義は以下のようになっている。

    trait Sql {
....
        def connection = play.db.DB.getConnection
....
        def apply(conn:java.sql.Connection=connection) = Sql.resultSetToStream(resultSet(connection))

つまり、Sqlトレイトが生成されるときには、必ずplay.db.DB.getConnection(これはJava側で定義)が呼び出されてConnectionが取得されるが、applyの引数が与えられない場合にはこのConnectionが使用される。デフォルトデータベースが指定されていないと、play.db.DB.getConnectionはエラー発生してしまう。

つまり、anormだけを取り出してplayとは無関係に使うような前提にはなっていない。あくまでも、playのアプリケーションは単一のデータベースを前提としており、自由なConnection指定は副次的なものとの位置づけとなる。

自前のjava.sql.Connectionの管理

ただし、自前のjava.sql.Connectionを扱うためには、次の二点をクリアしなければならない。

  • 一つのリクエスト・レスポンスサイクルの中で、一つのConnectionを使い回したいと思われるので、それをキャッシュする仕組みが必要。
  • レスポンスが終了したらそれをクローズするか、あるいは再利用のための準備をする必要がある。

前者は何とでもなる。後者については、@Finallyアノテーションを付けたメソッドのなかでThreadLocalに保存されたConnectionを廃棄(するなり初期化するなり)すればよい。

  @Finally
  def finallyProc = {
    Logger.info("clean-up")
  }
}