msawady’s engineering-note

なにも分からないエンジニアです。

【Scala】【Play Framework】Slick を使ってMySQLのDBに接続する

やっとデータベースへの接続を

  • ファイルの読み書きでやっていたデータストアをデータベースにする
  • MySQLデータベースへの接続
  • ORMとしてSlickを使う

MySQLデータベースへの接続

事前にテーブル作成、初期データ挿入

create table todo(
    id bigint(20) NOT NULL AUTO_INCREMENT,
    status varchar(31) NOT NULL,
    title varchar(255) NOT NULL,
    PRIMARY KEY (ID)) AUTO_INCREMENT=10000;

insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の要件定義');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の設計');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の実装');

10000始まりのauto increment にしたい場合はcreate table の末尾にAUTO_INCREMENT=10000 をつけます。 また、insertの際はidをnullで入れることでよしなにインクリメントしてくれます。

依存ライブラリの定義

build.sbtにjdbc, slick, mysql-connectorの依存関係を追加します。

libraryDependencies += jdbc
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.2.1"
libraryDependencies += "mysql" % "mysql-connector-java" % "6.0.6"

データベースの設定書き込み

conf/application.conf に接続先データベースを定義

todo-db = {
  url = "jdbc:mysql://localhost/todo_play?characterEncoding=UTF8&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"
  driver = com.mysql.jdbc.Driver
  user = "user"
  password = "password"
  connectionPool = disabled
}

url のパラメータには呪文が書かれていますが、、、

  • characterEncoding=UTF8&useUnicode=true : これは全角文字の文字化け防止
  • `useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC これをいれないとTimezoneのパースでエラーになります。
SQLException: The server time zone value '���� (�W����)' is unrecognized or represents more than one time zone.
You must configure either the server or JDBC driver (via the serverTimezone configuration property) 
to use a more specifc time zone value if you want to utilize time zone support.]

まったく理解してないですが、サーバーのtimezoneをちゃんと設定してあげる必要があるんだけど、呪文によってJDBCの設定で動くようにしているんじゃないかなーくらいに予想してます。 stackoverflow.com

RepositoryクラスでDatabaseの読み込み

import slick.driver.MySQLDriver.api._
.
.
private val database = Database.forConfig("todo-db")

MySQLDriverをインポートして、Database.forConfig("DB設定")をすることでセッションをはれます。

ORMとしてSlick利用

Entity クラスを編集

Slick によるマッピングができるよう、Domain Entityを編集します。

import slick.driver.MySQLDriver.api._

/**
  * entity class of todo
  */
case class Todo(id: Int, var status: TodoStatus, title: String)

class Todos(tag: Tag) extends Table[Todo](tag, "todo") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

  def status = column[TodoStatus]("status")

  def title = column[String]("title")

  def * = (id, status, title) <> (Todo.tupled, Todo.unapply)

}
object Todos extends TableQuery(new Todos(_))

case classに実装されるtupled, unapply を利用してデータの展開を行いますが、自然体だと

 def status = column[TodoStatus]("status")

自作したsealed traitのTodoStatusによってunapplyができません。

sealed traitにマッパーを実装

import slick.driver.MySQLDriver.api._

sealed trait TodoStatus

/**
  * todo status
  */
object TodoStatus {

  case object UNDONE extends TodoStatus

  case object DOING extends TodoStatus

  case object DONE extends TodoStatus

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

  def withName(s: String) = s.toLowerCase match {
    case "undone" => UNDONE
    case "doing" => DOING
    case "done" => DONE
  }

}

こんな感じでimplicit val としてマッパーを実装してあげると、unapplyが出来るようになります。

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

Repository クラスの実装

Slick の力を借りることで、とてもスッキリとした形でORマッピングが可能になっています。 可読性も高く、Repository クラスがビジネスロジックの実装に集中できる形ですね。

package services.repository

import com.google.inject.Singleton
import services.domain.{Todo, TodoStatus, Todos}
import slick.driver.MySQLDriver.api._

import scala.concurrent.Await
import scala.concurrent.duration.Duration

/**
  * repository class for todo management
  *
  */
@Singleton
class TodoRepository {

  private val database = Database.forConfig("todo-db")

  def getTodoList(): List[Todo] = {
    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList
  }

  def updateTodoList(ids: List[Int], status: TodoStatus) = {
    val toBeUpdated = Todos.filter(row => row.id inSetBind ids)
    database.run(toBeUpdated.map(_.status).update(status))
  }

}

Slickを利用すると、クエリの結果は全てFutureに包まれて帰ってきます。 結果を待ってからレスポンスを返すか、非同期にレスポンスを返すかは実装者の側で選択する必要があります。 ここでは、同期的に結果を待ってから返すようにしています。

    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList

感想

  • あっちこっちの設定で色々とハマりましたが、なんとかデータベースとの接続が出来ました。
  • Slick は設定さえできれば、Repositoryクラスをかなり綺麗に書けて便利だなぁという印象です。
    • DDL の生成とかも出来るようなので、先にテーブルを作ってしまったのはちょっと後悔。
  • Scalaapply, unapply, implicit など、Scala特有の?暗黙的なクラスやオブジェクトの仕組みも少しずつ分かってきた気がします。
  • 次は画面からのaddを実装したいですね。

以上です。なかなか苦しみましたが、非常に勉強になりました。