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をネストさせる。タプルをネストさせるといった手段が取られているようです。
次のバージョンで解決される…かも?

PlayFramework関西ビギナーズ in OsakanSpaceに参加してきました

表題の勉強会に参加してきました。

聴講中に残されていたメモをおいておきます。

以下メモ


PlayFramework 1.x

Scala
最近出てきた言語
JVM上で動く便利なやつ

JEE
小・中規模開発にはあまり向いていない・・・?
Javaはちょっとコードが冗長
StrutsをはじめとしてXml(設定ファイル)が糞ほどで生産されて面倒
➡つまり生産性が低い…

Play
何かと面倒臭いJEEを辞めた
さらばTomcat
環境によってホットリロードかどうかの切り替えももちろん可能
ステートレス
Javaはセッションを利用してサーバーサイドに情報を保存している
フルスタック(開発に必要なものは全部ぶちこんでいる)
Servletを利用したライブラリに関しては利用できない(JEEがないため)

なぜ今1.xなのか?
Scalaで書き直されているため読めないとつらい
Scalaは学習コストが高い…
1.3がリリースされる…という噂はある
現実的には1.xの方が…

まだまだ知名度が低く情報は少ない

  • JDKは1.5以上(1.xの話)
  • 2.x系は1.6以上

play1.x

  • 1.x系のテンプレートはGroovy

validation

  • @Requiredで簡単に入力値をチェック
  • 画面間でValidationの結果を受け渡すためにValidation.keepを利用する
  • messagesにてデフォルトのメッセージを変更
  • セッション ユーザーセッションが有効な間
  • フラッシュスコープ 次のリクエストにのみ持ち越し
  • キャッシュ JVM上に保持される.JEEで言うところのセッションに該当?memcachedで分散することが推奨されている模様
  • 1.x系JavaではDB周りもよしなにやってくれる…模様
  • ymlファイルにデータを打ち込んでおいてやればデータをサーバ起動時に流し込んでくれる
  • トランザクションはよしなにやってくれる
  • テーブルアノテーションという機能もある
  • カスケードで連動してくれるようになっている
  • CRUDの管理画面を提供してくれるモジュールcrud

play2.x
Scala

  • 手続き、オブジェクト、関数型のハイブリットな言語
  • 関数型で書くと並列処理でバグが生まれにくいうれしいメリット

ドキュメント 100ページ以上(playframework.org)
日本語版もあるよ

プラガブル
プラグインが充実している
最新版向けは27種類

静的な型付け
実行時にようやく分かるようなケアレスミスがなくなる。コンパイルで分かる。
ない関数は呼べない

play2.1は今年中…?
1系は2009年からのプロジェクトで安定している。Scalaもプラグインを利用すれば一応利用できる
2系は2012年から コアはScalaで書かれている Javaで言うところのサーブレットになっている…らしい
※勉強会から帰ったらRC1が公開されていました。胸が熱くなりますね

2.1での変更点
Scalaが最新に2.10?
JSONがもっと直感的に
ひな形作成がサポートされさらに簡単に!
ユニットテストやりすぎるとPlayが落ちると行った問題の解消
コンパイルが早くなる
AnormがデフォルトではなくなってScalaQueryに変更

今から始めるならなかな良い頃合い
単にWebアプリケーションを作ってみたい人にもおすすめ

ビーストハーレムはRC1からPlay2を利用
Squeryを利用しているが今となってはScalaQueryを使った方がいいんじゃないかといったところ

コンパイルはやっぱり遅い
古いマシンだと結構きつい 最新なマシンだとまし

ScalaでやるならPlayがよいのでは?Lift等他にもあることにはある

開発版を利用するとコンパイルが通らなくなるといったことが普通にある
Docomo,auは文字コードがWindows_?、SoftbankUTF-8でなかなか難しい

Scalaはコップ本、Playはドキュメント輪読をする
原則はペアプロを行う

やれモナドやれカリー化と関数型勉強会では言われるが一定の品質のものを作る分にはそこまでアカデミックな知識は問われない

ゲーム開発は保守の方が長く、リリースしてからがスタート
設定ファイルが複雑にならないというのは大きなメリット

新しいものに物怖じしない人がプロジェクト内に何人かいないとやっぱり厳しい…?



以上残されていたメモでした。
ちなみにScalaの勉強会もやるとかやらないとかって話があったので興味ある方は期待して待ちましょう

Play2.0:TODOリストを作成する

こちらで紹介されているTODOリストが最終的にどうなるかをまとめました
初めて作ったときにどこのソースに追加するのか少し迷ったので参考までに。

Application.scala

package controllers

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

object Application extends Controller {
  
  def index = Action {
    Redirect(routes.Application.tasks)
  }
  
  def tasks = Action {
    Ok(views.html.index(Task.all(), taskForm))
  }
  
  def newTask = Action { implicit request =>
    taskForm.bindFromRequest.fold(
      errors => BadRequest(views.html.index(Task.all(), errors)),
      label => {
        Task.create(label)
        Redirect(routes.Application.tasks)
      }
    )
  }
  
  def deleteTask(id: Long) = Action {
    Task.delete(id)
    Redirect(routes.Application.tasks)
  }
  
}

Task.scala

package models

import play.api.data._
import play.api.data.Forms._
import anorm._
import anorm.SqlParser._

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

object Task {

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

  val task = {
    get[Long]("id") ~ 
    get[String]("label") map {
      case id~label => Task(id, label)
    }
  }
  
  def all(): List[Task] = DB.withConnection { implicit c =>
    SQL("select * from task").as(task *)
  }
  
  def create(label: String) {
    DB.withConnection { implicit c =>
      SQL("insert into task (label) values ({label})").on(
        'label -> label
      ).executeUpdate()
    }
  }

  def delete(id: Long) {
    DB.withConnection { implicit c =>
      SQL("delete from task where id = {id}").on(
        'id -> id
      ).executeUpdate()
    }
  }
  
}

index.scala.html

@(tasks: List[Task], taskForm: Form[String])

@import helper._

@main("Todo list") {
    
    <h1>@tasks.size task(s)</h1>
    
    <ul>
        @tasks.map { task =>
            <li>
                @task.label
                
                @form(routes.Application.deleteTask(task.id)) {
                    <input type="submit" value="Delete">
                }
            </li>
        }
    </ul>
    
    <h2>Add a new task</h2>
    
    @form(routes.Application.newTask) {
        
        @inputText(taskForm("label")) 
        
        <input type="submit" value="Create">
        
    }
}

以上に加えて

  • conf/application.conf
  • conf/routes
  • conf/evolutions/default/1.sql

を編集することで、アプリケーションを起動できるようになるかと思います。
自分でアプリケーションを作っていろいろいじって遊んでみましょう!

近いうちにTODOリストへのテストやログイン画面の作成なんかをやります。たぶん。