Slick 2.0.xをPlay! framework2.2から使う


はじめに

タイトルにPlayも含めてるけど、今回のエントリーは主にSlick 2.0.xの話。

そして、最初に一言言わせて欲しい。Slickのドキュメントは分かりにくい or 不十分。ただ、「お前が貢献しろよ」、という真っ当な指摘が来そうなので、これに関してはあまり深入りはしない。

まずは今回の目的を。

目的・ゴール

簡単に今回の目標を書いておく。

  • Play! framework 2.2.xで、Slickを使ってDBにアクセスする。
  • DBのスキーマからコードを自動生成する機能を使う。

後者に関してだけど、DBのテーブルは生のSQLを使うことが多いので、あると非常に便利。幸い、Slickにはコード自動生成の機能があるので、それを使う。

ScalaのDB library

本題に入る前に、今回のエントリーの背景を少し。

今実験的に動いているプロジェクトでは、前回のプロジェクトと同じくPlay! 2.2を使うことにした。慣れてるし。でも、せっかくだし何か新しいことも試さないと、ということで、DBライブラリーは今まで使ったこと無いSlickを使うことにした。

今まではSquerylScalikeJDBCを使った事があって、どちらにも大きな不満はそれほどなかった。実際、両方共いいライブラリだと思う。後から出てきたScalikeJDBCの方が設計は綺麗だと思うけど。

その他、Play!の場合Anormという選択肢もあるけど、知り合いの信頼の置ける開発者から「論外」みたいな話を聞いたことがあるし、ドキュメントを見る限り基本的というか低レベルなので、あまり開発効率向上にはつながらなさそう、ということで除外。

環境

さて、本エントリーの環境。

  • Play! framework 2.2.2
  • play-slick 0.6.0.1 (Play!でSlickを使うためのプラグイン)
  • Slick 2.0.0 (play-slickのバージョンによって使えるSlickのバージョンが異なる)

以下、本題。

基本設定

dependencies

Build.scalaのdependenciesの設定は大体以下の通り。play-slickを含めておけば、Slickも勝手にインストールされる。ちなみに、今回は触れないけど、play-flywayプラグインも使っている。

  val appDependencies = Seq(
    jdbc,
    "com.typesafe.play" %% "play-slick" % "0.6.0.1",
    "org.slf4j" % "slf4j-nop" % "1.6.4",
    "postgresql" % "postgresql" % "8.4-702.jdbc4",
    "com.github.tototoshi" %% "play-flyway" % "1.0.3",
    cache
  )

Slickのコード自動生成を組み込み

これが結構時間がかかった。

参考にした情報

先ほどの公式のドキュメントから、設定サンプルへのリンクがあるんだけど、JDBCドライバー名やその他がハードコードされていたりして、若干残念。こStackOverflowにもこんなのがあったけど、基本的には同じような情報。

色々ググると、こんなサンプルプロジェクトが見つかった。さっきの公式ドキュメントから飛べるサンプルとこちらのサンプルを適当に組み合わせ、若干修正して使用することにした。

Build.scala

先に、Build.scalaの該当部分を載せて、後で説明する。

  // (1)
  val mainProject = play.Project(appName, appVersion, appDependencies).settings(defaultScalaSettings:_*).settings(
    slickCodeGen <<= slickCodeGenTask, // register manual sbt command
    sourceGenerators in Compile <+= slickCodeGenTask // register automatic source generator
  ).dependsOn(codegenProject)

  // (2)
  lazy val codegenProject = Project(
    id="slick-codegen",
    base=file("slick-codegen"),
    settings = Project.defaultSettings ++ Seq(libraryDependencies ++= appDependencies)
  )

  // (3) code generation task that calls the customized code generator
  lazy val slickCodeGen = TaskKey[Seq[File]]("gen-tables")
  lazy val slickCodeGenTask = (sourceManaged, dependencyClasspath in Compile, runner in Compile, streams) map { (dir, cp, r, s) =>
    val outputDir = (dir / "slick").getPath // place generated files in sbt's managed sources folder
    val pkg = "tv.kazu.fooproj.slick"
    toError(r.run("PlaySlickCodeGenerator", cp.files, Array(outputDir, pkg), s.log))
    val fname = outputDir + "/" + pkg.replaceAllLiterally(".", "/") + "/Tables.scala"
    Seq(file(fname))
  }

大雑把に言って、以下のことを行っている。

  • メインのPlayプロジェクト(1)以外に、コード自動生成の実行を行うプログラム用のプロジェクト(2)を作成
  • (1)→(2)という依存関係を持たせる
  • コード生成のsbtタスク(3)を作成

codegenプロジェクトの中身

上の例では、メインプロジェクトのルートフォルダの直下に、slick-codegen/src/main/というフォルダを作成して、その中にPlaySlickCodeGenerator.scala というファイルを以下の内容で作成している。以下、主要部分を抜粋。完全版はこちら

object PlaySlickCodeGenerator {

  def main(args: Array[String]) = {
    try {
      run(outputDir = args(0), pkg = args(1))
    } catch {
      case ex: Throwable => Logger.error("Could not generate code: " + ex.getMessage)
    } finally {
      Play.stop()
    }
  }

  private def run(outputDir: String, pkg: String) = {
    val cl = Thread.currentThread().getContextClassLoader

    // start fake application using in-memory database
    implicit val mainApp = FakeApplication(
      path = new File(".").getCanonicalFile,
      classloader = cl)

    Play.start(mainApp)

    // (snip)
    val db = Database(databaseName)

    // get list of tables for which code will be generated
    // also, we exclude the play evolutions table
    val excludedTables = Seq("play_evolutions", "schema_version")
    val model = db.withSession{
      implicit session =>
        val tables = db.driver.getTables.list.filterNot(t => excludedTables contains t.name.name)
        Logger.info("tables => " + tables)
        createModel( tables, db.driver )
    }

    // generate slick db code
    val codegen = new SourceCodeGenerator(model)

    codegen.writeToFile(
      profile = "scala.slick.driver." + db.driver.profile.toString,
      folder = outputDir,
      pkg = pkg,
      container = "Tables",
      fileName = "Tables.scala")

    Play.stop()
  }

}

/** Fake application needed for running evolutions outside normal Play app */
case class FakeApplication(
    override val path: java.io.File = new java.io.File("."),
    override val classloader : ClassLoader = classOf[FakeApplication].getClassLoader,
    val additionalConfiguration: Map[String, _ <: Any] = Map.empty) extends {
  override val sources = None
  override val mode = play.api.Mode.Test
} with Application with WithDefaultConfiguration with WithDefaultGlobal with WithDefaultPlugins {

  override def configuration =
    super.configuration ++ play.api.Configuration.from(additionalConfiguration)

}

コードを見れば、どんなことをしているか大体分かるとは思うけど、簡単に説明すると、

  • FakeApplicationを実行し、DB設定を取得(dbという変数)
  • createModelを呼んで、それをSourceCodeGeneratorに渡す
  • SourceCodeGeneratorを使ってファイルに書き込む

生成されたファイルは、上の例だと target/src_managed/slick/tv/kazu/fooproj/slick/Tables.scala に書き込まれる。

ScalikeJDBCなどと違って、全てのテーブルの情報がまとめてTables.scala に書き込まれる。

Slickを使った基本的なDB操作

ドキュメントを見ても分かりづらい and/or Slickのバージョンアップにドキュメントがついていっていない?ため、簡単な事をするのにもすごい時間がかかった。なので、ここにまとめておく。

Slickの中身は殆どみていないし仕組みにも詳しくないので、何か誤りがあれば指摘してもらえればと。

前提

上の方に書いたコード自動生成を使用することを前提に。

あと、ブログ向けにいちいち書き換えるのが面倒なので、実際のプロジェクトで使っているコード(の一部)をそのまま使用。

テーブルは以下の2つがある。

  • user: ユーザー情報 (id, name, etc)
  • user_facebook: ユーザーのfacebook関連情報 (facebookのユーザーID、access token etc.)

なので、生成されるクラスは以下の通り。

  • User, UserRow
  • UserFacebook, UserFacebookRow

参考URL

  • まずは公式ドキュメントへのリンクを。こっちも。
  • 少し古いバージョンだけど、こちらのページも参考になる。
  • slick-examplesという、オフィシャルのプロジェクトもある。が、これだけでは不十分。

select

主にfilterとかのメソッドを使う方法と、for文を使う方法の2通りがあるけど、最初は他のORMと似た感じの全社の方法から説明する。

その前に・・・詳しくは調べていないけど、以下のimportがないと色々エラーになったので、忘れずに。

import play.api.db.slick.Config.driver.simple._

通常のselect … from … where は以下のとおり。まずはID指定等、1 or 0レコードが返ってくる場合。

db.withSession { implicit session =>
  val facebookId = 1234567890L
  val accessToken = UserFacebook.filter(_.facebookId === facebookId).map(_.accessToken).firstOption
}

firstOptionはSqueryl の headOption と同様。戻り値はOption型。

複数レコードが返ってくる可能性がある場合は、firstOptionではなくlistメソッドを使う。戻り値はList。

val validUsers = User.filter(_.isValid === true).list

JOINする場合、以下のようにfor文を使ってカッコよくかける。

    db.withSession { implicit session =>
      val q = for {
        u <- User
        uf <- UserFacebook if u.id === uf.userId && uf.facebookId === 1234567890L
      } yield (u, uf)
      val user = q.firstOption.map(t => Foo(t._1, t._2)) // FooはUserRow, UserFacebookRowを引数として受け取る
    }

forの中のif文は、JOINの結合条件とWHEREの条件の両方が書いてある。分けてかけないかな・・・

update

selectと同様の文法で対象行及び列を取得し、それに対してupdateメソッドを呼ぶ。

    db.withSession { implicit session =>
      val q = UserFacebook.filter(_.facebookId === 1234567890L).map(_.accessToken)
      q.update("new access token")
    }

insert

これがちょっとトリッキーだった。

userテーブルに行を追加するには、UserRowクラスのインスタンスを作成する必要がある。そこまではシンプルな話だし問題ないんだけど、userテーブルのidがauto incrementの場合にどうすればいいのか、分かるまでに結構時間がかかった。

結論から言うと、auto incrementのカラムには、値をセットする必要はない(ダミーな値をセットすれば良い)。

    db.withTransaction { implicit session =>
      val now = new java.sql.Timestamp((new Date()).getTime())
      val userId =
        (User returning User.map(_.id)) += UserRow(0, ”Johne Doe”, now)
      UserFacebook += UserFacebookRow(userId, 1234567890L, Some("done"), now)
      userId
    }

MLのこのスレッドが役に立つかも。

まとめ

Scalaで使えるDB関連のライブラリにはいくつか種類があるが、今回はSlickを使ってみた。

Play framework 2.2からSlick 2.0.xを使うには、play-slickプラグインを使う。

DB関連のコードを自動生成するためには、Build.scalaとかにコードをある程度書かないといけないので、若干面倒。

Slickは簡潔に型安全なクエリーが書けて便利。ただ、ドキュメントが少ないor分かりづらい。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です