Upload page content

You can upload content for the page named below. If you change the page name, you can also upload content for another page. If the page name is empty, we derive the page name from the file name.

File to load page content from
Page name
Comment

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")
  }
}