• 15
    いいね
  • 0
    コメント

アカウンティング・サース・ジャパン Advent Calendar 2016 の7日目なのだ。

新しめの Play Framework で Slick を使ってみようとするとサクッと写経できるチュートリアルがない。ということで Slick Plugin for Play のサンプルコードを参考に試行錯誤した結果をチュートリアルとしてメモっとく。

こんなのができあがるので Play を勉強し始めたくらいの人でも Slick と連携する雰囲気がつかめるかと思う。

play-slick-app-home.png

なお時間がある人にはビズリーチさんのハンズオンがオススメ。

https://github.com/bizreach/play2-hands-on

開発環境を準備する

まずは開発環境を準備しよう。必要なものは下記の2つだ。

Play 2.5 を使用するには JDK 8 が必要になるので JDK 8 をインストールする。ターミナルで下記のコマンドを実行して Java のバージョンが 1.8 以降であることを確認しよう。

$ java -version
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

さらに Play 2.5 のプロジェクトを作成するために Activator が必要なのでインストールする。macOS であれば Homebrew で下記のようにインストールできる。

$ brew install typesafe-activator

ターミナルで下記のコマンドを実行して Activator のヘルプが表示されることを確認しよう。

$ activator -h
Usage: activator [options]

  Command:
  ui                 Start the Activator UI
.
.
.

これで開発環境の準備は完了だ。

Slick Plugin for Play のサンプルコードを実行してみる

この記事では Slick Plugin for Play のサンプルコードを参考にほぼゼロからプロジェクトを構築していくが、できあがるものはサンプルコードとほぼ同じになる。完成するものをイメージできた方がいいと思うので、それを最初に実行してみることにしよう。

ターミナルで下記のコマンドを実行してみよう。なお activator の初回実行時に依存性が解決されるので実行に数分かかると思う。

$ git clone https://github.com/playframework/play-slick
$ cd play-slick
$ activator
> project basic-sample
[basic-sample] $ run
.
.
.

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.NettyServer - 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 にアクセスしてみよう。データベースのマイグレーションが必要なため下記のようなエラーが表示される。

play-slick-basic-sample-evolution.png

Apply this script now! という赤いボタンをクリックしてデータベースをマイグレーションしよう。そうすると下記のような画面が表示される。

play-slick-basic-sample-home.png

猫と犬を登録できるフォームがそれぞれ表示される。どちらも名前と色を入力して登録すると下に一覧されるようになる。

新規 Play プロジェクトを作成する

さてサンプルコードをスクラッチから作っていこう。

下記のコマンドで新規の Play プロジェクトが作成できる。ちなみに play-slick-app がプロジェクト名になる。

$ activator new play-slick-app play-scala

この状態でサーバーを起動してみよう。この実行も数分かかるので気長に待とう。

$ cd play-slick-app
$ activator run
.
.
.

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.NettyServer - 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 にアクセスして下記の画面が表示されることを確認しよう。

play-slick-app-initial.png

Slick Plugin for Play を追加する

Play で Slick を使用するためのプラグインの依存性を build.sbt に追記する。

build.sbt
@@ -7,9 +7,11 @@ lazy val root = (project in file(".")).enablePlugins(PlayScala)
 scalaVersion := "2.11.7"

 libraryDependencies ++= Seq(
-  jdbc,
   cache,
   ws,
-  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test
+  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test,
+  "com.typesafe.play" %% "play-slick" % "2.0.0",
+  "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0",
+  "com.h2database" % "h2" % "1.3.176"
 )

この変更の意味は下記のとおり。今回は準備を簡単にするために H2 データベースをインメモリで使用する。

  • jdbcplay-slick と衝突するので削除
  • play-slick はプラグイン本体
  • play-slick-evolutions はデータベースのマイグレーション管理
  • h2 はデータベースドライバー

この状態で activator run を実行してエラーが表示されないことを確認しておこう。

Evolutions でデータベースのテーブルを作成する

下記の内容で conf/evolutions/default/1.sql という名前のファイルを作成する。この記事ではサンプルコードの猫だけを実装することにする。

conf/evolutions/default/1.sql
# --- !Ups

CREATE TABLE "CAT" (
    "NAME" VARCHAR NOT NULL PRIMARY KEY,
    "COLOR" VARCHAR NOT NULL
);

# --- !Downs

DROP TABLE "CAT";

この状態で下記のコマンドを実行する。

$ activator
[play-slick-app] $ h2-browser
[play-slick-app] $ run

h2-browser を実行したタイミングで下記のような H2 ブラウザの画面が開かれる。

h2-browser-login.png

JDBC URLjdbc:h2:mem:play に設定して Connect をクリックすると下記のような画面に遷移する。

h2-browser-tables-1.png

まだデータベースにはテーブルが作成されていないことがわかる。この状態で run を実行して、http://localhost:9000 にアクセスするとマイグレーションが必要というメッセージが出るはずだ。サンプルコードを実行したときのように Apply this script now! という赤いボタンをクリックしてデータベースをマイグレーションしておこう。

マイグレーションが完了したら H2 ブラウザのウィンドウに戻ってリロードする。そうすると下記のように CAT のテーブルが作成されていることがわかる。

h2-browser-tables-2.png

DAO と DTO を定義する

まずは猫を表現する DTO をケースクラスを使用して定義する。

app/models/Models.scala
package models

case class Cat(name: String, color: String)

猫テーブルにアクセスするための DAO を定義する。サンプルコードをまるっとコピーしてきたものがこれだ。

app/dao/CatDAO.scala
package dao

import scala.concurrent.Future

import javax.inject.Inject
import models.Cat
import play.api.db.slick.DatabaseConfigProvider
import play.api.db.slick.HasDatabaseConfigProvider
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import slick.driver.JdbcProfile

class CatDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._

  private val Cats = TableQuery[CatsTable]

  def all(): Future[Seq[Cat]] = db.run(Cats.result)

  def insert(cat: Cat): Future[Unit] = db.run(Cats += cat).map { _ => () }

  private class CatsTable(tag: Tag) extends Table[Cat](tag, "CAT") {

    def name = column[String]("NAME", O.PrimaryKey)
    def color = column[String]("COLOR")

    def * = (name, color) <> (Cat.tupled, Cat.unapply _)
  }
}

DAO のクラス内に Slick におけるテーブル定義がプライベートクラスとして記述されている。all メソッドと insert メソッドの内容は Slick の Getting Started を見ると詳細を理解できるかと。

この中で難解なのはインジェクションされる dbConfigProvider と DAO が継承している HasDatabaseConfigProvider[JdbcProfile] と突然登場する db だろう。Slick の Getting Started だと Database.forConfig() で作成して db に束縛しているのだけど Play と Slick でのサンプルでは異なる。

端的に言えば下記のようなことが行われていて、db 自体は Getting Started と同様に利用できるものなのだ。

  1. データベース設定を提供する DatabaseConfigProvider がインジェクションされる
  2. HasDatabaseConfigProvider でデータベース設定が取得される
  3. HasDatabaseConfigProvider の親トレイトの HasDatabaseConfig でデータベース設定から db が定義される

これで Slick のドキュメント眺めつつ DAO を書き始められるんじゃないだろうか。Slick の学習はそれはそれで大変だけどね……。

猫をデータベースに追加してみる

PlayConsole から猫をデータベースに追加してみることにする。マイグレーションを自動実行するために下記のようなコマンドで Activator を起動してコンソールにアクセスする。

$ activator console -Dplay.evolutions.db.default.autoApply=true

PlayConsole の Launch the interactive console を参考に Play アプリケーションをコンソール上でスタートさせる。

> import play.api._
> val env = Environment(new java.io.File("."), this.getClass.getClassLoader, Mode.Dev)
> val context = ApplicationLoader.createContext(env)
> val loader = ApplicationLoader(context)
> val app = loader.load(context)
> Play.start(app)

猫の DAO と DTO をインポートする。そして CatDAO のインスタンスを取得して、猫を一匹追加してみる。このあたりの手順はサンプルコードのテストコードを参考にした。

> import dao.CatDAO
> import models.Cat
> import scala.concurrent.Await
> import scala.concurrent.duration.DurationInt
> val app2dao = Application.instanceCache[CatDAO]
> val dao = app2dao(app)
> val cat = new Cat("Tama", "White")
> Await.result(dao.insert(cat), 1.second)
> Await.result(dao.all, 1.second)
res2: Seq[models.Cat] = Vector(Cat(Tama,White))

PlayConsole からの追加は少し面倒だけど、ここまでの作業がうまくいってることを確認するためにやってみた。

ビューとコントローラーを定義する

さてビューとコントローラーを定義しよう。といってもビューとコントローラーについては Play の話になってくる。このあたりは Play のドキュメントをつまみ食いしながら理解できるかと思うので駆け足でいく。基本はサンプルコードからのコピペで猫の部分だけ実装している。

まずはルーティング。フォームの POST 先を作成する。サンプルの URL は RESTful じゃないけど目をつむろう。

conf/routes
@@ -4,6 +4,7 @@

 # An example controller showing a sample home page
 GET     /                           controllers.HomeController.index
+POST    /insert/cat                 controllers.HomeController.insertCat
 # An example controller showing how to use dependency injection
 GET     /count                      controllers.CountController.count
 # An example controller showing how to write asynchronous code

次にコントローラー。HomeController.scala を下記のように編集する。CatDAO はインジェクションするので class の行を変更するのを忘れないように。

controllers/HomeController.scala
@@ -4,12 +4,19 @@ import javax.inject._
 import play.api._
 import play.api.mvc._

+import dao.CatDAO
+import models.Cat
+import play.api.data.Form
+import play.api.data.Forms.mapping
+import play.api.data.Forms.text
+import play.api.libs.concurrent.Execution.Implicits.defaultContext
+
 /**
  * This controller creates an `Action` to handle HTTP requests to the
  * application's home page.
  */
 @Singleton
-class HomeController @Inject() extends Controller {
+class HomeController @Inject() (catDao: CatDAO) extends Controller {

   /**
    * Create an Action to render an HTML page with a welcome message.
@@ -17,8 +24,22 @@ class HomeController @Inject() extends Controller {
    * will be called when the application receives a `GET` request with
    * a path of `/`.
    */
-  def index = Action {
-    Ok(views.html.index("Your new application is ready."))
+  def index = Action.async {
+    catDao.all().map {
+      cats => Ok(views.html.index(cats))
+    }
+  }
+
+  def insertCat = Action.async { implicit request =>
+    val cat: Cat = catForm.bindFromRequest.get
+    catDao.insert(cat).map(_ => Redirect(routes.HomeController.index))
   }

+  val catForm = Form(
+    mapping(
+      "name" -> text(),
+      "color" -> text()
+    )(Cat.apply)(Cat.unapply)
+  )
+
 }

次にビュー。index.scala.html を下記の内容に置換する。ここは理解しやすい内容かと思う。

views/index.scala.html
@(cats: Seq[Cat])

@main("Cat database") {
<div>
  <div id="cats">
    <h2>Insert a kitty cat here:</h2>

    <form action="/insert/cat" method="POST">
      <input name="name" type="text" placeholder="name your feline friend"/>
      <input name="color" type="text" placeholder="enter the color of this kitty cat"/>
      <input type="submit"/>
    </form>

    <h2>Previously inserted cats:</h2>
    <table>
      <tr><th>Name</th><th>Color</th></tr>
      @for(c <- cats){ 
      <tr><td>@c.name</td><td>@c.color</td></tr>
      }
    </table>
  </div>
</div>
}

プロジェクトを実行する

起動して http://localhost:9000 にアクセスしよう。

$ activator run -Dplay.evolutions.db.default.autoApply=true

下記のような画面が表示されて、フォームから猫を追加できるはずだ。

play-slick-app-home.png


この記事では Slick Plugin for Play をネタに Play Framework と Slick の連携をスクラッチから書く手順をチュートリアルっぽく書いてみた。僕自身 Slick を Play で動作させることすらできずに一度挫折しているので、Play を始めた人のお役に立てれば幸いだ。

参考文献

https://www.playframework.com/documentation/2.5.x/PlayConsole
https://www.playframework.com/documentation/2.5.x/PlaySlick
https://www.playframework.com/documentation/2.5.x/Developing-with-the-H2-Database
http://slick.lightbend.com/doc/3.1.1/
https://github.com/playframework/play-slick
https://github.com/bizreach/play2-hands-on/