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

Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Play Framework2.1.4(+Slick)を使ってみる

Scala PlayFramework

櫻です。

前回はRuby on Railsを触ってみました。
rubyRailsもすばらしいのですが、静的な型付けが欲しくなる事もあります。

ということで今回はPlay Framework(2.1.4+Slick)で前回のサンプルを作成してみましょう。


今回は以下のサイトをベースにサンプルを作成しました。
http://www.playframework.com/documentation/2.1.x/ScalaTodoList
http://www.playframework-ja.org/documentation/2.1.3/ScalaTodoList

準備

JDK(6以降)がインストールされていればOKです。

[sakura@MACmini250301 ~]$ java -version
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)
[sakura@MACmini250301 ~]$ 

インストール

Macでhomebrewを導入している場合は

$ brew install play

でインストールできます。それ以外の環境の場合は
http://www.playframework.com/からzipファイルをダウンロードしてきて展開+パスの設定をすれば完了です。
詳細はこちらを参考にしてください。


※プロキシを利用している場合は
環境変数HTTP(S)_PROXYを設定しましょう。

export HTTP_PROXY=http://proxy.example.com:8080/
export HTTPS_PROXY=http://proxy.example.com:8080/

正しく設定できれば、play helpで以下のようなヘルプが表示されます。

[sakura@MACmini250301 ~]$ play help
Getting net.java.dev.jna jna 3.2.3 ...
:: retrieving :: org.scala-sbt#boot-jna
	confs: [default]
	1 artifacts copied, 0 already retrieved (838kB/13ms)
Getting play console_2.9.2 2.1.4 ...
:: retrieving :: org.scala-sbt#boot-app
	confs: [default]
	29 artifacts copied, 0 already retrieved (5976kB/33ms)
Getting Scala 2.9.2 (for console)...
:: retrieving :: org.scala-sbt#boot-scala
	confs: [default]
	4 artifacts copied, 0 already retrieved (20090kB/30ms)
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org
Welcome to Play 2.1.4!

These commands are available:
-----------------------------
license            Display licensing informations.
new [directory]    Create a new Play application in the specified directory.

You can also browse the complete documentation at http://www.playframework.org.
[sakura@MACmini250301 ~]$ 

プロジェクト作成

プロジェクト作成は

$ play new blog

で行えます。
途中でアプリ名とScala/Javaのどちらを利用するかを聞かれます。

[sakura@MACmini250301 play_sample]$ play new blog
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org

The new application will be created in /Users/sakura/Documents/play_sample/blog

What is the application name? [blog]
> 

Which template do you want to use for this new application? 

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 1
OK, application blog is created.

Have fun!

[sakura@MACmini250301 play_sample]$ cd blog/
[sakura@MACmini250301 blog]$ 

プロジェクトの作成で以下のファイルが作成されています。

[sakura@MACmini250301 blog]$ tree -a
.
├── .gitignore
├── README                        作成したアプリのREADME
├── app                           アプリのソース
│   ├── controllers                 コントローラ
│   │   └── Application.scala
│   └── views                       ビュー
│       ├── index.scala.html
│       └── main.scala.html
├── conf                          アプリの設定ファイルなど
│   ├── application.conf            設定ファイル
│   └── routes                      ルーティング定義
├── project                       プロジェクトの設定ファイル
│   ├── Build.scala
│   ├── build.properties
│   └── plugins.sbt
├── public                        assets(JavaScript、css、画像などブラウザから参照するファイル)
│   ├── images
│   │   └── favicon.png
│   ├── javascripts               JavaScript
│   │   └── jquery-1.9.0.min.js
│   └── stylesheets               css
│       └── main.css
└── test                          単体テストコード
    ├── ApplicationSpec.scala
    └── IntegrationSpec.scala

10 directories, 14 files
[sakura@MACmini250301 blog]$ 

自動で.gitignoreも作成されるのが嬉しいですね。
この状態でplayを実行するとplay consoleが起動します。

[sakura@MACmini250301 blog (master)]$ play
Getting org.scala-sbt sbt 0.12.2 ...
:: retrieving :: org.scala-sbt#boot-app
	confs: [default]
	40 artifacts copied, 0 already retrieved (8381kB/141ms)
[info] Loading project definition from /Users/sakura/Documents/play_sample/blog/project
[info] Set current project to blog (in build file:/Users/sakura/Documents/play_sample/blog/)
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org

> Type "help play" or "license" for more information.
> Type "exit" or use Ctrl+D to leave this console.

[blog] $ 

play console内でrunを実行すると開発モードでサーバが起動します。

[blog] $ run

Getting Scala 2.10.0 ...
:: retrieving :: org.scala-sbt#boot-scala
	confs: [default]
	5 artifacts copied, 0 already retrieved (24106kB/366ms)
[info] Updating {file:/Users/sakura/Documents/play_sample/blog/}blog...
[info] Done updating.                                                        
--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

ブラウザでhttp://localhost:9000/を開くと画像のような画面が開きます。

f:id:IntelligentTechnology:20130919162525p:plain

開発モードではソースコードを書き換えてブラウザで読み込むだけで自動でコンパイルされて実行されます。

サーバを停止させる時はCtrl-Dです。

デバッグ

デバッガを接続する場合はplay実行時にdebugを指定すればOKです。
デフォルトでは9999番ポートで待ち受けるようです。

[sakura@MACmini250301 blog (master)]$ play debug
Listening for transport dt_socket at address: 9999
[info] Loading project definition from /Users/sakura/Documents/play_sample/blog/project
[info] Set current project to blog (in build file:/Users/sakura/Documents/play_sample/blog/)
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org

> Type "help play" or "license" for more information.
> Type "exit" or use Ctrl+D to leave this console.

[blog] $ 

IDEの設定ファイル生成

さらに、play console内でeclipseやideaコマンドを実行すると、IDE用のプロジェクトファイルが生成されます。

[blog] $ idea with-sources=yes
[info] Trying to create an Idea module blog
[info] Excluding folder targett_2.10;2.1.4 ...      
[info] Created /Users/sakura/Documents/play_sample/blog/.idea/IdeaProject.iml
[info] Created /Users/sakura/Documents/play_sample/blog/.idea
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/cache
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes_managed
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/native_libraries
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/resolution-cache
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/resource_managed
[info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/streams
(commons-codec_commons-codec_1.6_test,List(commons-codec_commons-codec_1.3))
(org.apache.httpcomponents_httpcore_4.1.3_test,List(org.apache.httpcomponents_httpcore_4.0.1))
(org.apache.httpcomponents_httpclient_4.1.2_test,List(org.apache.httpcomponents_httpclient_4.0.1))
[info] Created /Users/sakura/Documents/play_sample/blog/.idea_modules/blog.iml
[info] Created /Users/sakura/Documents/play_sample/blog/.idea_modules/blog-build.iml
[blog] $ 

これで、必要なクラスパス等が設定された状態のプロジェクトが作成されています。

Hello World

まずはHello Worldを作成してみましょう。
app/controllers/Application.scalaのindexメソッドを以下の様に書き換えます。

object Application extends Controller {
  def index = Action {
    Ok("Hello World")
  }
}

ファイルを保存してhttp://localhost:9000/を開くと無事Hello Worldが表示されます。

f:id:IntelligentTechnology:20130919162931p:plain

ルーティングの設定…?

Railsではこの後ルーティングの設定を行いましたが、Play Frameworkで同じ事を行うと、コントローラに対応するメソッドが無いというコンパイルエラーが発生します。
これをエラーとするかどうかが、静的/動的型付けの発想の違いですね。

とはいえ、全てのアクションを定義するまで他の部分も動作できなくなるのは大変なのでダミーのアクションを作成してコンパイルを通す事が可能です。
conf/routeファイルに以下を追加します。

GET         /posts                   controllers.Application.posts
POST        /posts                   controllers.Application.newPost
GET         /posts/new               controllers.Application.showNewPost
GET         /posts/:id               controllers.Application.showPost(id: Long)
POST        /posts/:id/delete        controllers.Application.deletePost(id: Long)

この時点でhttp://localhost:9000/postsへアクセスすると以下の様にコンパイルエラーになります。

playコンソール

[info] Compiling 8 Scala sources and 1 Java source to /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes...
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:8: value newPost is not a member of object controllers.Application
[error] POST        /posts                   controllers.Application.newPost
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:9: value showNewPost is not a member of object controllers.Application
[error] GET         /posts/new               controllers.Application.showNewPost
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:10: value showPost is not a member of object controllers.Application
[error] GET         /posts/:id               controllers.Application.showPost(id: Long)
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:7: value posts is not a member of object controllers.Application
[error] GET         /posts                   controllers.Application.posts
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:11: value deletePost is not a member of object controllers.Application
[error] POST        /posts/:id/delete        controllers.Application.deletePost(id: Long)
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:7: value posts is not a member of object controllers.Application
[error] GET         /posts                   controllers.Application.posts
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:8: value newPost is not a member of object controllers.Application
[error] POST        /posts                   controllers.Application.newPost
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:9: value showNewPost is not a member of object controllers.Application
[error] GET         /posts/new               controllers.Application.showNewPost
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:10: value showPost is not a member of object controllers.Application
[error] GET         /posts/:id               controllers.Application.showPost(id: Long)
[error] /Users/sakura/Documents/play_sample/blog/conf/routes:11: value deletePost is not a member of object controllers.Application
[error] POST        /posts/:id/delete        controllers.Application.deletePost(id: Long)
[error] 10 errors found
[error] (compile:compile) Compilation failed
[error] application - 

ブラウザ
f:id:IntelligentTechnology:20130920110725p:plain

コンパイルが通る様にコントローラに対応するアクションメソッドを追加します。

object Application extends Controller {
  def index = Action {
    Ok("Hello World")
  }

  def posts = TODO
  def newPost = TODO
  def showNewPost = TODO
  def showPost(id: Long) = TODO
  def deletePost(id: Long) = TODO

}

この状態でhttp://localhost:9000/posts などへアクセスするとTODO(ダミー)画面が表示されます。

f:id:IntelligentTechnology:20130919173424p:plain

モデル作成

Slickの追加・設定

Play Framework 2.1ではDBアクセスまわりはデフォルトでanormというライブラリを利用しますが、2.3からはSlickというライブラリに変更されるようなので、今回はSlickを使ってみます。
Slick関連は
http://gan.hatenablog.jp/entry/2013/06/09/133204
http://slick.typesafe.com/doc/1.0.1/lifted-embedding.html#tables
を参考にしました。
必要なライブラリの依存をproject/Build.scalaに追加します。

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    "com.typesafe.play" %% "play-slick" % "0.4.0"
  )

playコンソールを起動し直すと自動でライブラリがダウンロードされます。
IDEの設定を生成している場合は、再作成する必要があるかもしれません。

続いて、DBの接続設定もしておきます。
conf/application.confファイルに定義があります。
デフォルトではH2データベース用の設定がコメントアウトされているので、これを有効にします。

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.user=sa
db.default.password=""

さらにSlick用の設定も追加しておきます。

# Slick
slick.default="models.*"

Postモデルの作成

Railsではrailsコマンドでモデルクラスのひな形を作成しましたが、Play Frameworkではクラス定義を元にいろいろ行ってくれるようです。
Slickのサンプルをみるとモデルはcase classとして作成し、DBアクセス周りは専用のobjectを作成するようです。
ということで、app/models/Post.scalaは以下の様になります。

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

case class Post(id: Long, title: String, text: String)

object Posts extends Table[Post]("post") {
  def id = column[Long]("post_id", O.PrimaryKey, O.AutoInc)
  def title = column[String]("title")
  def text = column[String]("text")

  def * = id ~ title ~ text <> (Post, Post.unapply _)
  def ins = title ~ text returning id

  def all() = DB.withSession{ implicit session:Session =>
    Query(Posts).sortBy(_.id).list
  }

  def create(title: String, text: String) = DB.withSession { implicit session: Session =>
    Posts.ins.insert(title, text)
  }

  def delete(id: Long) = DB.withSession { implicit session: Session =>
    Posts.where(_.id === id).delete
  }

  def apply(id: Long): Post = DB.withSession { implicit session: Session =>
    Query(Posts).where(_.id === id).list.head
  }
}

id、title、textメソッドの定義がカラムの定義として使われる事になります。

  def * = id ~ title ~ text <> (Post, Post.unapply _)
  def ins = title ~ text returning id

この辺りはDSLの様に見えますが、ただのscalaプログラムです。
今回はLifted Embeddingと呼ばれるスタイルでDB操作を記述していますが、SQLを直接記述するスタイルもあります。

val post_id = 10
val post = sql"select * from post where post_id = $post_id".as[Post].head

一覧画面の作成

一覧画面のviewを作成します。
Play Frameworkでは専用のテンプレート言語でviewを作成します。@の後ろだけ特殊な動作を行います。
app/views/index.scala.htmlファイルを以下の様に変更します。
1行めの@(posts: List[Post])はこのテンプレートの引数になります。アクションや他のテンプレートから値を受け取る事になります。

@(posts: List[Post])

@import helper._

@main("Listing posts") {

    <h1>Listing posts</h1>

    <table>
    @posts.map { post =>
        <tr>
            <td><a href="@routes.Application.showPost(post.id)">@post.title</a></td>
            <td>
            @form(routes.Application.deletePost(post.id)) {
                <input type="submit" value="Delete">
            }
            </td>
        </tr>
    }
    </table>

    <a href="@routes.Application.showNewPost">New Post</a>
}

続いてアクションを作成します。

app/controllers/Application.scalaを以下の様に変更します。

import追加

import models._

postsメソッド変更

  def posts = Action {
    Ok(views.html.index(Posts.all()))
  }

views.html.index(...)の読み出しがテンプレートの実行になります。
Posts.all()メソッドの結果がビューの引数として渡されます。
両者の型がそろっているのがポイントで、指定の間違いや開発中にテンプレートへ渡す型を変更した場合不整合があればコンパイルエラーとして検出されます。

これでhttp://localhost:9000/posts へアクセスすると、DBアクセス時にテーブルが存在しないため、エラーになります。
エラーにはなりますが、きちんと必要なSQLが提示され、画面のボタンを押すと自動で実行してくれます。またSQLファイルもconf/evolutions.[dbname]/[n].sqlに保存されています。

f:id:IntelligentTechnology:20130919173516p:plain

「apply this script now」ボタンを押すと空の一覧画面が表示されます。

f:id:IntelligentTechnology:20130919164112p:plain

新規作成画面

次に新規作成の画面のviewを作成します。
app/views/newPost.scala.htmlを以下の様に作成します。

@(postForm: Form[(String, String)])

@import helper._

@main("New Post") {

    <h1>Add a new Post</h1>

    @form(routes.Application.newPost) {

        @inputText(postForm("title"))
        @inputText(postForm("text"))

        <input type="submit" value="Create">

    }

    <a href="@routes.Application.posts()">Post List</a>

}

アクションも作成します。app/controllers/Application.scalaにimportを追加し、

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

showNewPostメソッドの変更とpostFormの定義を追加します。

  def showNewPost = Action {
    Ok(views.html.newPost(postForm))
  }

  val postForm = Form(
    tuple(
      "title" -> nonEmptyText,
      "text" -> nonEmptyText
    )
  )

このFormの定義で入力必須などの指定が可能です。

ファイルを保存し、一覧画面のNew Postをクリックすると入力画面が表示されます。

f:id:IntelligentTechnology:20130919171923p:plain

続いて投稿のアクションを作成します。
newPostメソッドを以下の様に変更します。

  def newPost = Action { implicit request =>
    postForm.bindFromRequest.fold(
      errors => BadRequest(views.html.newPost(errors)),
      form => {
        Posts.create(form._1, form._2)
        Redirect(routes.Application.posts())
      }
    )
  }

フォームの内容を受け取り、エラーがあれば入力画面へもどし、なければDBへ登録して一覧画面へ遷移させます。

ファイルを保存し、入力画面で適当に入力後createボタンを押すと一覧に追加されます。

f:id:IntelligentTechnology:20130919172013p:plain

詳細画面

詳細画面も作成しましょう。
app/views/post.scala.htmlを以下の様に作成します。

@(post: Post)

@import helper._

@main(post.title) {
    <p><strong>Title:</strong> @post.title</p>
    <p><strong>Text:</strong> @post.text</p>

    <a href="@routes.Application.posts()">Post List</a>
}

app/controllers/Application.scalaのshowPostメソッドを以下の様に変更します。

  def showPost(id: Long) = Action {
    Ok(views.html.post(Posts(id)))
  }

ファイルを保存して一覧画面のリンクをクリックすると、詳細が表示されます。

f:id:IntelligentTechnology:20130919172119p:plain

削除ボタン

最後に削除機能を作成します。

app/controllers/Application.scalaのdeletePostメソッドを以下の様に変更します。

  def deletePost(id: Long) = Action {
    Posts.delete(id)
    Redirect(routes.Application.posts())
  }

ファイルを保存し、一覧画面のdeleteボタンを押すと、投稿が削除されます。


プロジェクト全体のコードはこちらにあります。

まとめ

Play Frameworkで簡単なサンプルを作成しました。
お手軽さではRailsの方が上かもしれませんが、型のチェックが入るのは大きな安心感が生まれるのではないでしょうか。