Play Frameworkをいろいろと弄っていたのですが、その中でScala用のDBライブラリであるSlickを利用してみました。 実際にSlickでデータ取得ができるようになるまでの流れを、備忘録的にここに記します。
目次
前提
- Play Frameworkのsbtプロジェクトを作成済みである
- DBはPostgreSQL 13を利用する
- Play Frameworkのバージョンは2.8.8である ※sbt1.5.5環境で直前の2.8.7等を使用するとバグで動作しないようなので注意が必要。参考: https://www.utakata.work/entry/scala/sbt1-5-5-playframework-twirl
依存関係の追加
build.sbtに依存関係を追加していきます。
Play FrameworkでSlickを使うためには、Play Slickライブラリを追加します。また、Play Slickを追加するとSlickのライブラリも依存関係に追加されるのですが、バージョンが古いようなので今回は明示的にSlickライブラリ(およびHikariCP用の実装)を追加します。
後述するコードジェネレータのライブラリCodegen、PostgreSQLのドライバも追加します。
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()
参考文献
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