Slick でテーブル定義のコードを自動生成する

Slick 2系では experimental 扱いではありますが、コード生成の機能が含まれていて、 めんどうなテーブル定義のコードを自動生成することができます。ちょっとやってみましょう。

テスト用のテーブル、こんな感じです。

slickcodegenexample=# CREATE TABLE users (
  id serial PRIMARY KEY,
  first_name varchar(100) NOT NULL,
  last_name varchar(100) NOT NULL,
  birth_date timestamp NOT NULL
);

コード生成機能は slick 本体の jar ではなく、slick-codegen と言う jar に含まれています。使用しているデータベースに合わせた jdbc driver も必要です。それぞれ build.sbt の libraryDependencies に加えて下さい。

name := """slick-codegen-example"""

version := "1.0"

scalaVersion := "2.11.1"

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "2.1.0",
  "com.typesafe.slick" %% "slick-codegen" % "2.1.0",
  "org.postgresql" % "postgresql" % "9.3-1102-jdbc41"
)

コード生成のためのコードは以下のような単純なもので、SourceCodeGenerator.main というメソッドを呼ぶだけです。 気をつけることは jdbc driver と slick のほうの driver を合わせることくらいでしょうか。今回は PostgreSQL を使っているので scala.slick.driver.PostgresDriver を選びます。

package com.example

import scala.slick.codegen.SourceCodeGenerator

object SlickCodegen {

  def main(args: Array[String]): Unit = {
    val slickDriver = "scala.slick.driver.PostgresDriver"
    val jdbcDriver = "org.postgresql.Driver"
    val url = "jdbc:postgresql://localhost/slickcodegenexample"
    val outputFolder = "src/main/scala"
    val pkg = "com.example.models"

    SourceCodeGenerator.main(
      Array(
        slickDriver,
        jdbcDriver,
        url,
        outputFolder,
        pkg
      )
    )
  }
}

実行すると、outPutFolder で指定した場所にコードが生成されます。 見てみるといつも書いてるようなコードですね。

package com.example.models

object Tables extends {
  val profile = scala.slick.driver.PostgresDriver
} with Tables

trait Tables {
  val profile: scala.slick.driver.JdbcProfile
  import profile.simple._
  import scala.slick.model.ForeignKeyAction
  import scala.slick.jdbc.{GetResult => GR}

  lazy val ddl = Users.ddl

  case class UsersRow(id: Int, firstName: String, lastName: String, birthDate: java.sql.Timestamp)

  implicit def GetResultUsersRow(implicit e0: GR[Int], e1: GR[String], e2: GR[java.sql.Timestamp]): GR[UsersRow] = GR{
    prs => import prs._
    UsersRow.tupled((<<[Int], <<[String], <<[String], <<[java.sql.Timestamp]))
  }

  class Users(_tableTag: Tag) extends Table[UsersRow](_tableTag, "users") {
    def * = (id, firstName, lastName, birthDate) <> (UsersRow.tupled, UsersRow.unapply)
    def ? = (id.?, firstName.?, lastName.?, birthDate.?).shaped.<>({r=>import r._; _1.map(_=> UsersRow.tupled((_1.get, _2.get, _3.get, _4.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))
    val id: Column[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
    val firstName: Column[String] = column[String]("first_name", O.Length(100,varying=true))
    val lastName: Column[String] = column[String]("last_name", O.Length(100,varying=true))
    val birthDate: Column[java.sql.Timestamp] = column[java.sql.Timestamp]("birth_date")
  }

  lazy val Users = new TableQuery(tag => new Users(tag))
}

データベースの timestamp 型が java.sql.Timestamp 型にマッピングされています。java.sql.Timestamp 型では少々使いにくい。Java 8 日時 API or joda-time が良さそうと思いますが、そういえば私は slick-joda-mapper というのを作っていました。これで timestamp 型を org.joda.time.DateTimeマッピングさせましょう。

https://github.com/tototoshi/slick-joda-mapper

slick-codegen でのコード生成はこういう時のためにカスタマイズが可能になっています。まず先ほどの build.sbt に joda-time と slick-joda-mapper の依存性も加えます。

name := """slick-codegen-example"""

version := "1.0"

scalaVersion := "2.11.1"

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "2.1.0",
  "com.typesafe.slick" %% "slick-codegen" % "2.1.0",
  "org.postgresql" % "postgresql" % "9.3-1102-jdbc41",
  "joda-time" % "joda-time" % "2.4",
  "com.github.tototoshi" %% "slick-joda-mapper" % "1.2.0"
)

コード生成のためのコードは次のようになります。 SourceCodeGenerater を継承して、"java.sql.Timestamp""DateTime" に変更しています。

import scala.slick.{model => m}
import scala.slick.codegen.SourceCodeGenerator
import scala.slick.driver.JdbcProfile

class CustomSourceCodeGenerator(model: m.Model) extends SourceCodeGenerator(model) {

  override def code = "import com.github.tototoshi.slick.PostgresJodaSupport._\n" + "import org.joda.time.DateTime\n" + super.code

  override def Table = new Table(_) {
    override def Column = new Column(_) {
      override def rawType = model.tpe match {
        case "java.sql.Timestamp" => "DateTime"
        case _ => {
          super.rawType
        }
      }
    }
  }
}

object CodeGen extends App {

  val slickDriver = "scala.slick.driver.PostgresDriver"
  val jdbcDriver = "org.postgresql.Driver"
  val url = "jdbc:postgresql://127.0.0.1/slickcodegenexample"
  val outputFolder = "src/main/scala"
  val pkg = "com.example.models"

  val driver: JdbcProfile = scala.slick.driver.PostgresDriver

  val db = {
    driver.simple.Database.forURL(url, driver = jdbcDriver)
  }

  db.withSession { implicit session =>
    new CustomSourceCodeGenerator(driver.createModel()).writeToFile(slickDriver, outputFolder, pkg)
  }

}

生成されたコードを見ると、さっき java.sql.Timestamp だったところが DateTime に変わっていることがわかります。

package com.example.models

object Tables extends {
  val profile = scala.slick.driver.PostgresDriver
} with Tables

trait Tables {
  val profile: scala.slick.driver.JdbcProfile
  import profile.simple._
  import com.github.tototoshi.slick.PostgresJodaSupport._
  import org.joda.time.DateTime
  import scala.slick.model.ForeignKeyAction

  import scala.slick.jdbc.{GetResult => GR}

  lazy val ddl = Users.ddl

  case class UsersRow(id: Int, firstName: String, lastName: String, birthDate: DateTime)

  implicit def GetResultUsersRow(implicit e0: GR[Int], e1: GR[String], e2: GR[DateTime]): GR[UsersRow] = GR{
    prs => import prs._
    UsersRow.tupled((<<[Int], <<[String], <<[String], <<[DateTime]))
  }

  class Users(_tableTag: Tag) extends Table[UsersRow](_tableTag, "users") {
    def * = (id, firstName, lastName, birthDate) <> (UsersRow.tupled, UsersRow.unapply)
    def ? = (id.?, firstName.?, lastName.?, birthDate.?).shaped.<>({r=>import r._; _1.map(_=> UsersRow.tupled((_1.get, _2.get, _3.get, _4.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))
    val id: Column[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
    val firstName: Column[String] = column[String]("first_name", O.Length(100,varying=true))
    val lastName: Column[String] = column[String]("last_name", O.Length(100,varying=true))
    val birthDate: Column[DateTime] = column[DateTime]("birth_date")
  }

  lazy val Users = new TableQuery(tag => new Users(tag))
}

slick-codegen は sbt に組み込んでコンパイル時にコード生成を行う、ということもでき、そのサンプルが https://github.com/slick/slick-codegen-example にあります。

まだ experimental なのでそこまでやるべきかは微妙なところですが、少なくともコード生成自体は普通に使えそうだなあと思いました。