読者です 読者をやめる 読者になる 読者になる

Play2.1RC+Scala+SlickでTODOチュートリアルを作成する

Play2.1からはデフォルトのORMがAnormからScalaQueryに変更されるって話を聞いたので
PlayFramework2.0のTODOチュートリアルをPlay2.1+Scala+Slickで書き直してみようと思います。

まず2.1RC1の導入ですがこちらから

Play2.0.4で作ったプロジェクトをPlay2.1RC1で利用できるようにするためにはいろいろ変更の必要があります。
が、今回は大人しくPlay2.1RC1で作成したアプリケーションを利用しましょう。

まずSlickの導入です。

object ApplicationBuild extends Build {

  val appName         = "SlickSample"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm,
    "com.typesafe" % "slick_2.10.0-RC1" % "0.11.2"
  )


  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here      
  )

}

appDependencies内に「"com.typesafe" % "slick_2.10.0-RC1" % "0.11.2"」を追加するだけです。簡単ですね。

ではSlickでチュートリアルで作成したTask.scalaを書き換えてみましょう。こうなります。

package models

import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

case class Task(id: Long, label: String)

object Tasks extends Table[Task]("TASK") {

  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def label = column[String]("LABEL", O.NotNull)
  def * = id ~ label <> (Task, Task.unapply _)
  def ins = label returning id

  def all(): List[Task] = connectDB {
    Query(Tasks).sortBy(_.id).list
  }

  def create(label: String) = connectDB {
    Tasks.ins.insert(label)
  }

  def delete(id: Long) = connectDB {
    Tasks.where(_.id === id).delete
  }

  def connectDB[Any](f: => Any): Any = {
    Database.forURL("jdbc:h2:mem:play", driver = "org.h2.Driver") withSession {
      f
    }
  }

}

変更点を上から順に追っていきましょう。
まずimport文です。
Slickを利用する際には下記の二つをインポートすればOKなようです。

import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

次にcase classです。
Slickではcase classを利用せず結果をタプルで返すことも出来ますが
今回は出来るだけ以前のソースコードを変更しないようそのままにしておきます。

そして次のobjectがSlickのキモの部分になります。

object Tasks extends Table[Task]("TASK") {

  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def label = column[String]("LABEL", O.NotNull)
  def * = id ~ label <> (Task, Task.unapply _)
  def ins = label returning id
}

この宣言でテーブルの定義が完了したも同然となります。
Anormで1.sqlで直に書くよりはすっきりした・・・かもしれません。
まずTableを継承することを忘れないように。

objectにdefで宣言されているid,labelがそれぞれTASKテーブルのカラムになります。
O.PrimaryKeyは主キーの宣言に用います。
なお、複合主キーである場合は宣言方法が異なります。
O.AutoIncを利用することでシーケンスを用いてIDを自動採番します。
O.NotNullはNotNull制約の宣言になります。

調べた限りではPrimaryKeyでないがUniqueであるカラムを宣言できないように見えます。
誰かご存知の方がいらしたらご教授ください。
2013/2/4追記

def * はパーサーでセレクト文を利用した際の戻り値の形式をここで決めているようです。

def insはインサートを行う際に利用します。
今回はシーケンスを利用した自動採番を用いているのでこれを宣言しています。

では次に実際にDBでCRUD操作を行う関数です。

  def all(): List[Task] = connectDB {
    Query(Tasks).sortBy(_.id).list
  }

  def create(label: String) = connectDB {
    Tasks.ins.insert(label)
  }

  def delete(id: Long) = connectDB {
    Tasks.where(_.id === id).delete
  }

  def connectDB[Any](f: => Any): Any = {
    Database.forURL("jdbc:h2:mem:play", driver = "org.h2.Driver") withSession {
      f
    }
  }

上記のように一切SQL文を記述することなくAnorm版と同様の結果を取り出せます。
まずconnectDBですがこれはDBアクセスの際必要となる記述を高階関数として宣言しています。
このあたりScalaの便利なところですね。

allではTASKテーブルに登録されている情報をID順に整列し、リスト化して取り出しています。
createではlabelをDBに登録します。
ちなみに以下のように書いても同じように動作します。

Tasks.label.insert(lable)

deleteはwhereによって条件を絞り込み削除。簡単ですね。

なお上記の変更に伴いApplication.scalaも一部変更になります。

package controllers

import play.api._
import play.api.mvc._

import play.api.data._
import play.api.data.Forms._

import models._

object Application extends Controller {

  val taskForm = Form(
    "label" -> nonEmptyText)

  def index = Action {
    Redirect(routes.Application.tasks)
  }

  def tasks = Action {
    Ok(views.html.index(Tasks.all(), taskForm))
  }

  def newTask = Action { implicit request =>
    taskForm.bindFromRequest.fold(
      errors => BadRequest(views.html.index(Tasks.all(), errors)),
      label => {
        Tasks.create(label)
        Redirect(routes.Application.tasks)
      })
  }

  def deleteTask(id: Long) = Action {
    Tasks.delete(id)
    Redirect(routes.Application.tasks)
  }

}

Taskとして呼び出していた部分をTasksに書き換えています。

なおAnormでは1.sqlがアプリケーション起動時に実行されていたので必要ありませんでしたが
今回はアプリケーション起動時にテーブルの作成をGlobal.scalaに記述します。
Global.scalaという名前でapp/に配置してください。

import play.api._
import models._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

import models._
import java.sql.{Date,Time,Timestamp}

object Global extends GlobalSettings {

  override def onStart(app: Application) {
    Logger.info("Application has started")

    Database.forURL("jdbc:h2:mem:play", driver = "org.h2.Driver") withSession {

      Tasks.ddl.create

    }

  }

  override def onStop(app: Application) {
    Logger.info("Application shutdown...")
  }

}

Tasks.ddl.createでテーブルの作成を行っています。

以上で行うことでSlickを利用してチュートリアルのTODOアプリケーションの完成です。

なかなか便利なSlickですがあまりドキュメントが見当たらず
こちらからテストなどを元に調査している状態です。

Slickを利用する上で注意しなければいけないのはテーブルにカラムが23以上存在する場合です。
23という数字でピンとくるかもしれませんがTuple22問題が直撃してます。
テーブルの宣言でタプル内に23個以上宣言するとコンパイルを通りません。
またcase classを利用した場合ですがこちらも23個以上フィールドをもてないのでアウトです。
これの対策は設計を見直すか、タプルなりcase classをネストするといった方法があるようです。

tuple22問題対策とUnique宣言する方法は調査中です。

追記

PrimaryKeyではないがUniqueを利用したい場合について
以下のようにindexを定義してやることでUniqueをつけることできます。

  def idx = index("idx_a",label,unique = true)

Tuple22問題の解決策について
case classをネストさせる。タプルをネストさせるといった手段が取られているようです。
次のバージョンで解決される…かも?