slick-codegenを使ってテーブルコードを自動生成する。そしてPostgresqlへCRUDする。

2016.08.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

「プログラムからどのようにしてデータベースへアクセスするか」は常に検討課題にあがります。いまやOracleやMySQLにとどまらず、本当に多くの選択肢がデータ永続化の候補として存在しますが、「RDBをデータベースとして選択し、O/Rマッパーを使ってプログラムから制御する」という考え方は今後も生き続けることでしょう。

今回はそんな状況で少しでも楽できることを示すのが目的です。例として、データベースはPostgresql、プログラムはScalaを選択したときに、slick-codegenを使うことで、CRUDを行うためのお膳立てを自動化してみます。

使うもの

以下の構成で行きます。Herokuの無料Postgresqlを使います。

項目 内容
フレームワーク Playframework
言語 Scala
O/R mapper Slick 3
Database Postgresql (on Heroku)

やること

以下の順で進めていきます。

  1. Postgresqlのテーブルを作成する
  2. slick-codegenを使ってテーブルコードを自動生成する
  3. 生成したコードを使ってCRUD操作してみる

Postgresqlのテーブルを作成する

まずはHeroku上でPostgresqlを使えるようにします。

10_HerokuでPostgresql

次に、接続情報を用いて、手元のIntellijからPostgresqlへ接続します。このとき、以下SSLに関するプロパティの設定が必要であることに注意してください。

  • ssl true
  • sslmode require
  • sslfactory org.postgresql.ssl.NonValidatingFactory

20_IntellijからHerokuのデータベースへ接続する 21_SSL関連の設定を行う

データベースへ接続できたら、テーブルを作成します。Intellijの力を借りつつ以下のようにUserテーブルとTaskテーブルを作成しました。

User

CREATE TABLE "user"
(
    id INTEGER PRIMARY KEY NOT NULL,
    username VARCHAR(50) NOT NULL,
    age INTEGER,
    address TEXT
);
CREATE UNIQUE INDEX user_id_uindex ON "user" (id)

Task

CREATE TABLE task
(
    id INTEGER PRIMARY KEY NOT NULL,
    user_id INTEGER NOT NULL,
    taskname VARCHAR(500) NOT NULL,
    enddate DATE,
    CONSTRAINT task_user_id_fk FOREIGN KEY (user_id) REFERENCES "user" (id)
);
CREATE UNIQUE INDEX task_id_uindex ON task (id)

これで、ソースコード生成対象のテーブル作成がおわりました。複数テーブル作成しましたが、今回は例示ということでUserテーブルのソースコードを生成します。

slick-codegenを使ってテーブルコードを自動生成する

Userテーブルを自動生成します。まず、sbtの設定ファイルを以下のようにしてください。

build.sbt

libraryDependencies ++= Seq(
  jdbc,
  cache,
  ws,
  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test,
  "com.typesafe.slick" % "slick-codegen_2.11" % "3.1.1", // -- ①
  "org.postgresql" % "postgresql" % "9.4.1208",          // -- ②
  "com.typesafe.play" %% "play-slick" % "2.0.0"
)
  • ① slick-codegen: テーブルコード生成のためのライブラリです。
  • ② postgresql: テーブルコードを生成するためには生成対象のテーブルに合ったドライバが必要になります。今回は Postgresqlですのでこれを指定します。

自動生成するコードを書きます。非常に簡単で、Postgresqlへ設定するための情報を定義したら、それをSourceCodeGeneratorに渡すだけで済みます。

package infrastructures.slick.generator

import slick.codegen.SourceCodeGenerator

object SlickModelGenerator {

  def main(args: Array[String]): Unit = {
    val slickDriver = "slick.driver.PostgresDriver"   // -- ①
    val jdbcDriver = "org.postgresql.Driver"          // -- ②
    val url =                                         // -- ③
      """jdbc:postgresql://xxxxx:5432
        |/task_management
        |?ssl=true
        |&sslfactory=org.postgresql.ssl.NonValidatingFactory""".stripMargin
    val user = "y.wada"
    val password = "f8wjfowebnskf"

    val outputFolder = "app"                          // -- ④
    val pkg = "infrastructures.models"                // -- ⑤

    SourceCodeGenerator.main(
      Array(
        slickDriver,
        jdbcDriver,
        url,
        outputFolder,
        pkg,
        user,
        password
      )
    )
  }
}
  • ①: Slickのドライバを指定します。
  • ②: JDBCのドライバを指定します。
  • ③: 接続先URLを指定します。スキーマ名まで含める必要があること、先でIntellijから接続する際に設定したSSL関係の情報をパラメータとして記述する必要があることに注意してくdささい。
  • ④: 出力先ディレクトリを指定します。パッケージ名ではないことに注意してください。
  • ⑤: パッケージを指定します。④と合わせると、app/infrastructures/models に出力されます。そして、package infrastructures.modelsが付与された状態となります。

準備完了です。実行します。

30_generate

これで、infrastructures.modelsTables.scalaが生成されるはずです。

生成したコードを使ってCRUD操作してみる

Tablesが生成されれば、それを使ってCRUD操作ができるようになります。Controllerに記述して動作を確認してみましょう。

application.conf

slick.dbs.default {
  driver = "slick.driver.PostgresDriver$"
  db.driver = "org.postgresql.Driver"
  db.url = "jdbc:postgresql:/xxxxx:5432/xxxxx?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory"
  db.user = "username"
  db.password = "password"
  db.connectionTimeout = 10000
}

UserController.scala

package controllers.user

import javax.inject.Inject

import core.util.FutureSupport
import infrastructures.models.Tables
import infrastructures.models.Tables.UserRow
import play.api.db.slick.DatabaseConfigProvider
import play.api.libs.json.{ JsPath, Reads }
import play.api.mvc.{ Action, Controller }
import slick.driver.JdbcProfile

import scala.concurrent.ExecutionContext

class UserController @Inject()(
    dbConfigProvider: DatabaseConfigProvider
)(implicit ec: ExecutionContext) extends Controller {

  import slick.driver.PostgresDriver.api._
  import RequestBodyConverters._

  val dbConfig = dbConfigProvider.get[JdbcProfile]

  /*ここにCRUD相当のメソッドを記述していきます*/

}

object RequestBodyConverters {

  import play.api.libs.functional.syntax._

  implicit val userReads: Reads[User] = (
      (JsPath \ "username").read[String] and
          (JsPath \ "address").readNullable[String] and
          (JsPath \ "age").readNullable[Int]
      ) (User)
}

case class User(
    userName: String,
    address: Option[String],
    age: Option[Int]
)

Create

def get(userId: Int) = Action.async { req =>
  val result = dbConfig.db.run(Tables.User.filter(_.id === userId).result)
  result.map(user => Ok(user.head.username))
}

Read

def insert = Action.async(parse.json) { req =>
  for {
    addUser <- FutureSupport.jsResultToFuture(req.body.validate[User])
    insertAction = Tables.User += UserRow(0, addUser.userName, addUser.age, addUser.address)
    result <- dbConfig.db.run(insertAction)
  } yield Created
}

Update

def update(userId: Int) = Action.async(parse.json) { req =>
  for {
    user <- FutureSupport.jsResultToFuture(req.body.validate[User])
    insertUpdate = Tables.User.update(Tables.UserRow(
      userId,
      user.userName,
      user.age,
      user.address
    ))
    result <- dbConfig.db.run(insertUpdate)
  } yield NoContent
}

Delete

def delete(userId: Int) = Action.async { req =>
  val result = dbConfig.db.run(Tables.User.filter(_.id === userId).delete)
  result.map(_ => NoContent)
}

試す

POSTメソッドでinsertします。ChromeエクステンションのDHCというツールを使ってPOSTしています。

40_post 50_result

データが作成されていることが確認できました。Auto incrementもちゃんと機能しています。

おわりに

slick-codegenを使って、テーブルコードを自動生成し、それを使ってCRUD操作を行うことができました。ORマッパーは便利な反面、利用を開始するまでの手続きやお作法が多く、面食らうことも多いため、今回のようにツールにまかせて自動化できるところはどんどん自動化してやりましょう。我々が本当に書きたいコードはデータベースに接続するコードではなく、データベースに保存されたデータを用いてビジネスロジックを組み立てる方です。皆様のコーディングの効率化に少しでもお役に立てれば幸いです。

参考