技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Playframework2.5 で Slick3 弄ってみる

まずはアプリケーション仕様

今回は特に何も考えず、Evolution で突っ込んだ文字列を画面に表示するだけ。
ジンプルなことは良いことだ。

教科書はこれ

PlaySlick - 2.5.x

Playframework プロジェクト作成

いつものこと

    activator new

play-scala を選択する。
現時点で正式公開なのは、Play 2.5 なので、2.5 のテンプレができる。

厳密には 2.5.10 ができた(2017/3/9 22時現在)。

まずは写経してみる

liblaryDependencies に突っ込むところからとな。
当然 evolution も突っ込む。

build.sbt にて

val H2_VERSION = "1.4.193"

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

次は DB 設定。
application.conf に下記を追加。

slick.dbs.default.driver="slick.driver.H2Driver$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:mem:play"

db ディレクティブは参照しないらしい。
そっかー的な感覚。

そして、ここまでで起動と。

sbt run

するとエラー

1) A binding to play.api.db.DBApi was already configured at play.api.db.slick.evolutions.EvolutionsModule.bindings(EvolutionsModule.scala:15):
Binding(interface play.api.db.DBApi to ConstructionTarget(class play.api.db.slick.evolutions.internal.DBApiAdapter) in interface javax.inject.Singleton) (via modules: com.google.inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$1).
  at play.api.db.DBModule.bindings(DBModule.scala:25):
Binding(interface play.api.db.DBApi to ProviderConstructionTarget(class play.api.db.DBApiProvider)) (via modules: com.google.inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$1)

写経してこれとか不親切が過ぎんだろ。
これに対する答えが

PlaySlickFAQ - 2.5.x

It is very likely that you have enabled the jdbc plugin, and that doesn’t really make sense if you are using Slick for accessing your databases. To fix the issue simply remove the Play jdbc component from your project’s build.

なんか「Slack使ってんのになんでJDBCなんぞ有効にしてんだよ…」的なニュアンスを感じるが…。
まぁいい、build.sbt を修正

val H2_VERSION = "1.4.193"

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

Evolution を設定する

Play Slick supports Play database evolutions.

とのことなので、早速ページを見る。

Evolutions - 2.5.x

  1. conf/evolutions/default ディレクトリを作成
  2. (番号).sql を作成
# Example table schema
# --- !Ups

CREATE TABLE IF NOT EXISTS `messages` (
    `id`         bigint(20) NOT NULL AUTO_INCREMENT,
    `message`    varchar(255) NOT NULL,
    `created_at` datetime NOT NULL,
    `updated_at` datetime NOT NULL,
    PRIMARY KEY (id)
);

INSERT INTO `messages` (`message`, `created_at`, `updated_at`)
  VALUES ('Example message', '2017-03-09 13:03:41', '2017-03-09 13:03:41');

# --- !Downs

DROP TABLE IF EXISTS messages;

いざ実行!

Database 'default' needs evolution!

よっしゃ〜!!……?
いつまでたってもEvolution が終わらない…(汗

ログみてなるほど、Evolution 実行後にアプリケーションが再起動してる…つまりそのタイミングでインメモリデータベースも…
設定書き換え。

slick.dbs.default.driver="slick.driver.H2Driver$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:./db/play"

これで Evolution 通過。

DBアクセス

ということで、スキーマと Dao 作る

package models

import java.sql.Timestamp
import javax.inject.{Inject, Singleton}

import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import slick.driver.JdbcProfile
import slick.lifted.ProvenShape
import scala.concurrent.duration._

import scala.concurrent.{Await, Future}

case class Message(
  id: Long,
  message: String,
  createdAt: Timestamp = new Timestamp(System.currentTimeMillis()),
  updatedAt: Timestamp = new Timestamp(System.currentTimeMillis())
)

@Singleton
class Tables @Inject() (override protected val dbConfigProvider: DatabaseConfigProvider)
  extends HasDatabaseConfigProvider[JdbcProfile] {

  import driver.api._

  class Messages(tag: Tag) extends Table[Message](tag, "messages") {
    def id      = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def message = column[String]("message")
    def createdAt = column[Timestamp]("created_at")
    def updatedAt = column[Timestamp]("updated_at")

    override def * : ProvenShape[Message] =
      (id, message, createdAt, updatedAt) <> (Message.tupled, Message.unapply)
  }

  val messages = TableQuery[Messages]

  def getAllMessages(): Seq[Message] = {
    import dbConfig.driver.api._
    Await.result(
      db.run(messages.sortBy(_.id).result),
      10 seconds
    )
  }
}

そしたらController から読んでやる。

package controllers

import javax.inject._

import models.Tables
import play.api.mvc._

@Singleton
class HomeController @Inject() (
                               tables: Tables
                               ) extends Controller {

  def index = Action {
    Ok(views.html.index(tables.getAllMessages().map(_.message).mkString(",")))
  }
}

さぁって…と思って試して見ると

JdbcSQLException: テーブル "messages" が見つかりません

What's!?

仕方ないので、ファイルを H2 Database Engine よりダウンロードしてコンソールを起動。
そこで疑問氷解。

どうにもテーブル名やカラム名が大文字になってる。当然、見つからないと言われたSQL(ログから取得) select "id", "message", "created_at", "updated_at" from "messages" order by "id" も通らない。
何これヒデエ(TT

試しにコンソール上で大文字かして見る。

> select ID, MESSAGE, CREATED_AT, UPDATED_AT from "MESSAGES" order by ID;

ID      MESSAGE     CREATED_AT      UPDATED_AT  
1  Example message 2017-03-09 13:03:41.0   2017-03-09 13:03:41.0
(1 行, 9 ms)

FUUUUxxKK!!

OK わかったよ。

@Singleton
class Tables @Inject() (override protected val dbConfigProvider: DatabaseConfigProvider)
  extends HasDatabaseConfigProvider[JdbcProfile] {

  import driver.api._

  class Messages(tag: Tag) extends Table[Message](tag, "MESSAGES") {
    def id      = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def message = column[String]("MESSAGE")
    def createdAt = column[Timestamp]("CREATED_AT")
    def updatedAt = column[Timestamp]("UPDATED_AT")

    override def * : ProvenShape[Message] =
      (id, message, createdAt, updatedAt) <> (Message.tupled, Message.unapply)
  }

  val messages = TableQuery[Messages]

  def getAllMessages(): Seq[Message] = {
    import dbConfig.driver.api._
    Await.result(
      db.run(messages.sortBy(_.id).result),
      10 seconds
    )
  }
}

これで実行成功...自宅で落ち着いてやると案外すんなり行くんだよねこういうの。