プログラマブル深海魚

目立たないけど華やかに

Play Framework上でSlickを使う

Play Frameworkをいろいろと弄っていたのですが、その中でScala用のDBライブラリであるSlickを利用してみました。 実際にSlickでデータ取得ができるようになるまでの流れを、備忘録的にここに記します。

目次

前提

依存関係の追加

build.sbtに依存関係を追加していきます。 Play FrameworkでSlickを使うためには、Play Slickライブラリを追加します。また、Play Slickを追加するとSlickのライブラリも依存関係に追加されるのですが、バージョンが古いようなので今回は明示的にSlickライブラリ(およびHikariCP用の実装)を追加します。
後述するコードジェネレータのライブラリCodegenPostgreSQLのドライバも追加します。

libraryDependencies += "com.typesafe.play" %% "play-slick" % "5.0.0"
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.3.3"
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % "3.3.3"
libraryDependencies += "com.typesafe.slick" %% "slick-codegen" % "3.3.3"
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.23"

DBの用意

適当に以下のようなDBを作成します。

CREATE TABLE user_info
(
    user_id serial NOT NULL,
    first_name text,
    last_name text,
    age integer CHECK (age >= 0),
    CONSTRAINT user_id_pkey PRIMARY KEY (user_id)
)

INSERT INTO user_info (first_name, last_name, age)
values ('Mano', 'Sakuragi', '16'), ('Hiori', 'Kazano', '15'), ('Meguru', 'Hachimiya', '16');

DB接続設定

デフォルトのDB接続設定をconf/application.confに記述します。
HikariCPによるコネクションプールを利用する設定と利用しない設定をできるようですが、今回はHikariCPを利用するようにします。numThreads, maxConnectionsの設定は適当なため、削っても問題なし。
他のDBであってもprofile, driverを該当のDBのものに差し替えれば動作するはずですが、未確認。

slick.dbs = {
  default = {
    profile = "slick.jdbc.PostgresProfile$"
    db = {
      connectionPool = "HikariCP"
      dataSourceClass = "slick.jdbc.DatabaseUrlDataSource"
      properties = {
        driver = "org.postgresql.Driver"
        url = "jdbc:postgresql://<ホスト名>:<ポート番号>/<DB名>"
        user = "<ユーザ名>"
        password = "<パスワード>"
      }
      numThreads = 20
      maxConnections = 20
    }
  }
}

コードジェネレータを使う

コードジェネレータを使うと、データベーススキーマを操作するためのコードが生成されます。
本来は別なScalaプログラムを書いて実行するか、あるいはsbtタスクを自分で定義するかでコードジェネレータを使うようですが、今回は横着して有志の方が作られているsbtプラグインを利用します。
GitHub - tototoshi/sbt-slick-codegen: slick-codegen compile hook for sbt

project/plugin.sbtに以下を追記します。

addSbtPlugin("com.github.tototoshi" % "sbt-slick-codegen" % "1.4.0")
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.23"

build.sbtには設定を追加します。
自動生成クラスはslickCodegenOutputPackageのパッケージ下に出力されます。

Compile / sourceGenerators += slickCodegen
slickCodegenDatabaseUrl := "jdbc:postgresql://localhost:5432/mydb"
slickCodegenDatabaseUser := "postgres"
slickCodegenDatabasePassword := "postgres"
slickCodegenDriver := slick.jdbc.PostgresProfile
slickCodegenJdbcDriver := "org.postgresql.Driver"
slickCodegenOutputPackage := "entities"
slickCodegenExcludedTables := Seq("schema_version")

sbtシェルからslickCodegen(またはcompile)を実行すると、target/scala-2.13/src_managed/mainにTables.scalaが出力されるかと思います。

実際に使ってみる

ここまで来ればSlickを使えるようになっているはずです。
例として、ユーザの取得と追加を作成してみました。

package controllers

import entities.Tables.{UserInfo, UserInfoRow}
import play.api._
import play.api.mvc._
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.api.libs.json._
import play.api.libs.json.Json
import slick.jdbc.PostgresProfile
import slick.jdbc.PostgresProfile.api._

import javax.inject.Inject
import scala.concurrent.ExecutionContext

class UserController @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, cc: ControllerComponents)(implicit ec: ExecutionContext)
    extends AbstractController(cc) with HasDatabaseConfigProvider[PostgresProfile] {
  case class UserInfoModel(userId: Option[Int], firstName: Option[String], lastName: Option[String], age: Option[Int])
  private implicit val recordReads: Reads[UserInfoModel] = Json.reads[UserInfoModel]
  private implicit val recordWrites: Writes[UserInfoModel] = Json.writes[UserInfoModel]

  def getUserAll: Action[AnyContent] = Action.async { implicit request =>
    val action = UserInfo.result
    db.run(action)
      .map(users => {
        Ok(Json.obj(
          "users" -> users.map(u => UserInfoModel(Some(u.userId), u.firstName, u.lastName, u.age))
        ))
      })
  }

  def getUser(id: Int): Action[AnyContent] = Action.async { implicit request =>
    val action = UserInfo.filter(_.userId === id).result.head
    db.run(action)
      .map(user => {
        Ok(Json.obj(
          "user" -> UserInfoModel(Some(user.userId), user.firstName, user.lastName, user.age)
        ))
      })
  }

  def addUser: Action[JsValue] = Action.async(parse.json) { implicit request =>
    val body = request.body.validate[UserInfoModel]
    body.map(data => {
      val action = (UserInfo returning UserInfo.map(_.userId) into ((u, id) => u.copy(userId = id))) +=
        UserInfoRow(0, data.firstName, data.lastName, data.age)
      db.run(action)
        .map(newUser => {
          Ok(Json.obj(
            "user" -> UserInfoModel(Some(newUser.userId), newUser.firstName, newUser.lastName, newUser.age)
          ))
        })
    }).get
  }
}

conf/routesには以下を追記します。

GET     /user/all                   controllers.UserController.getUserAll()
GET     /user/:id                   controllers.UserController.getUser(id: Int)
POST    /user                       controllers.UserController.addUser()

POST /user (リクエスト)
POST /user (リクエスト)
POST /user (レスポンス)
POST /user (レスポンス)
GET /user/all
GET /user/all
GET /user/1
GET /user/1

参考文献

Slick
https://scala-slick.org/doc/3.3.3/

Play Slick - 2.8.x
https://www.playframework.com/documentation/2.8.x/PlaySlick

ScalaDB用ライブラリ、Slickを使い倒すハンズオン - Qiita
https://qiita.com/sonken625/items/bcfe5f78323a67205933

Scala Play Framework と Slick で Connection Pool を利用する - 猫でもわかるWebプログラミングと副業
https://www.utakata.work/entry/20190406/1554545383