slick-codegenの基本使用例、DEFAULT値の付いたTIMESTAMPをOption型として出力する

この記事は [Scala Advent Calendar 2017](https://qiita.com/advent-calendar/2017/scala) の 12月10日 の記事です。（すでに日がたっていますが枠が空いていたので飛び入りしました。）
`slick-codegen` に関する記事はすでに、[slick-codegenの利用例と中身の説明](https://esplo.net/2017/12/slick-codegen/)　で記述されており、ネタが被ったなーと思いましたが、自分が一番やりたかった `CURRENT_TIMESTAMP`のデフォルト値がついた`timestamp` の扱いに関する記述が無かったため、記事を書くことにしました。
なお、`Scala Advent Calendar`に参加するのは今回が初めてなので優しくして下さい（ガクブル

# はじめに
## 前提
`slick-codegen` とは、PlayでDataBaseに接続する時に使用するSlickの`Table`と入出力で使用するインスタンスの`case class`のコードを、DBのスキーマから自動生成してくれるツールです。
つまり、今まで手動で書いていたinfrastructure層のコードの一部を、DBのスキーマより自動化してくれる便利ツールです。

さて、`slick-codegen`ですが、現状いろいろな古いサンプル、h2のサンプルやPostgreSQLのサンプルがあり、MySQLで使いたいと思っても、自分も初めに調べた時に訳が分からなくなりました。
そこでまずは簡単に整理をしたいと思います。(`Slick 3.2.0`をベースに話します。)

最も簡単なサンプルは、 https://github.com/slick/slick-codegen-example や
 https://github.com/playframework/play-scala-isolated-slick-example にありますが、
MySQLではなくh2ベースです。しかも改造しようとするとこのソースでは build.sbt に記述されており面倒です。

## 最も基本的な使い方

なので初めに、
_カスタマイズや機能拡張なんて高度な事には興味ない！ いいから実行させてくれ！_
という人のために、最も基本的な書き方を提示します。

```build.sbt
import sbt._

lazy val root = (project in file("."))
  .settings(
    inThisBuild(List(scalaVersion := "2.12.4")),
    libraryDependencies ++= List(
      "com.typesafe.slick" %% "slick" % "3.2.0",
      "com.typesafe.slick" %% "slick-codegen" % "3.2.0",
      "mysql" % "mysql-connector-java" % "6.0.5"
    )
  )
```

url,user,passwordは各自の環境に合わせて下さい。

```SlickTableCodeGenerator.scala
package generator

import slick.codegen.SourceCodeGenerator

object SlickTableCodeGenerator extends App {
  SourceCodeGenerator.run(
    profile = "slick.jdbc.MySQLProfile",
    jdbcDriver = "com.mysql.cj.jdbc.Driver",
    url = "jdbc:mysql://localhost/database?useSSL=false&nullNamePatternMatchesAll=true",
    outputDir = "./output",
    pkg = "infrastructure.sample",
    user = Some("root"),
    password = Some(""),
    ignoreInvalidDefaults = true
  )
}
```

これだけで動きます。ねっ、簡単でしょう。

## どこを機能拡張するべきかを調べる

改造するには、元のソースを読むのが一番！　ということで、`run`メソッド内の以下を読みます。
https://github.com/slick/slick/blob/3.2.0/slick-codegen/src/main/scala/slick/codegen/SourceCodeGenerator.scala#L71-L72

```scala
val m = Await.result(db.run(profileInstance.createModel(None, ignoreInvalidDefaults)(ExecutionContext.global).withPinnedSession), Duration.Inf)
new SourceCodeGenerator(m).writeToFile(profile,outputDir,pkg)
```

ここで行っていることは、`MySQLProfile.createModel`から`slick.model.Model`を生成し、それを`SourceCodeGen`(`slick-codegen`の事)に渡しているだけです。
つまりスキーマ自体は `MySQLProfile.createModel`が生成していることがわかります。

つまり、機能拡張対象には、`MySQLProfile`と`SourceCodeGen`が想定できます。

# カスタマイズ
## MySQLProfile を拡張する
さて、最初の話のtimestampのデフォルト値に話を戻します。
残念ながら、`CURRENT_TIMESTAMP`という形式は現在時刻という特殊なデフォルト値で、普通のデフォルト値によくある0やnullのように表現できません。（現在時刻なので当たり前ですね。）
そのため、MySQLProfileさんはこのデフォルト値を無視します・・・　値が入りません・・・
値が入らず関連するフラグすらもないため、SourceCodeGeneratorをいくら改造したところで意味がありません。

そこで、`Timestamp`のデフォルト値がある時にOption型が付与されるようにMySQLProfileで使われる`CustomColumnBuilder.tpe`を書き換えます。

`Timestamp`,`Date`,`Time`の型が来た時に、`Joda-Time`の`DateTime`,`LocalDate`,`LocalTime`に変換しています。さらに、`ColumnBuilder.rawDefault`メソッドでデフォルト値が存在するかがわかる(`Some("CURRENT_TIMESTAMP")`が返る)ため、デフォルト値がある場合はOption型で返すようにします。
SourceCodeGeneratorはDateTime型ではOption型が付与できる挙動のため、二重Optionにならないように`meta.typeName`も確認しています。

```scala 
  class CustomColumnBuilder(tableBuilder: TableBuilder, meta: MColumn) extends ColumnBuilder(tableBuilder, meta) {

    override def tpe: String =
      jdbcTypeToScala(meta.sqlType, meta.typeName).toString match {
        case "java.lang.String" => if (meta.size.contains(1)) "Char" else "String"
        case "java.sql.Timestamp" =>

          // If a [TimeStamp] default value is set such as CURRENT_TIMESTAMP, [Option] is added.
          rawDefault match {
            case Some(_) if meta.typeName.toUpperCase == "TIMESTAMP" => "Option[DateTime]"
            case _                                                   => "DateTime"
          }
        case "java.sql.Date" => "LocalDate"
        case "java.sql.Time" => "LocalTime"
        case jdbcType        => jdbcType
      }
  }
```

全体としては、`CustomColumnBuilder`をoverrideするために、以下のように`JdbcModelBuilder`、`MySQLProfile`のクラス、メソッドもoverrideします。

```scala
object CustomMySQLProfile extends MySQLProfile with CustomJdbcModelComponent {
  def RowNum(sym: AnonSymbol, inc: Boolean) = MySQLProfile.RowNum(sym, inc)

  def RowNumGen(sym: AnonSymbol, init: Long) = MySQLProfile.RowNumGen(sym, init)
}

trait CustomJdbcModelComponent extends JdbcModelComponent {
  self: JdbcProfile =>
  override def createModelBuilder(tables: Seq[MTable], ignoreInvalidDefaults: Boolean)(implicit ec: ExecutionContext): CustomJdbcModelBuilder =
    new CustomJdbcModelBuilder(tables, ignoreInvalidDefaults)
}

class CustomJdbcModelBuilder(mTables: Seq[MTable], ignoreInvalidDefaults: Boolean)(implicit ec: ExecutionContext)
  extends JdbcModelBuilder(mTables, ignoreInvalidDefaults) {

  override def createColumnBuilder(tableBuilder: TableBuilder, meta: MColumn): ColumnBuilder = new CustomColumnBuilder(tableBuilder, meta)

  class CustomColumnBuilder(tableBuilder: TableBuilder, meta: MColumn) extends ColumnBuilder(tableBuilder, meta) {(上記と同じため略)}

```

これでようやく、`CustomMySQLProfile.createModel` を使ってModelを出力できます。

## ModelデータからSchema情報を消す
この記事の一番やりたい事と外れますが、`CustomMySQLProfile.createModel` で出力されたコードは、よく見るとDB名（スキーマ）が入っていることがあります。
残念なことにDB名が入っていると、テストのために別のDBやh2のDBを使用した時にエラーで動かず、テストできません。
そこで以下のようなメソッドを作り、出力結果の`model: Model`から、`model.name.schema` をNoneで消します。

```scala 
  private[this] def createModelWithoutSchema(model: Model): Model = {
    val tables: Seq[Table] = model.tables.map((table: Table) => Table(
      name = QualifiedName(table = table.name.table, schema = None, catalog = table.name.catalog),
      columns = table.columns,
      primaryKey = table.primaryKey,
      foreignKeys = table.foreignKeys,
      indices = table.indices,
      options = table.options
    ))
    Model(tables, model.options)
  }
```

## SourceCodeGeneratorを拡張する

上記でこの記事の一番やりたかったtimestampのOption化はだいたい終わりましたが、`SourceCodeGenerator`に関してもまだ拡張する必要があります。
`Joda-Time`のimportを解決するために、`org.joda.time._`と`MySQLJodaSupport`を追加します。

また、`AUTO_INCREMENT`なカラムもOption化したいため、`Column.asOption = autoInc` を追加します。

他の記事でよく見る `def autoIncLastAsOption`は`@deprecated`のため使用しないほうが良いです。同じことをやるのであれば、`Table.autoIncLast = true`, `Column.asOption = autoInc`を追加しましょう。
なお、自分は`AUTO_INCREMENT`なカラムをcase classの変数のラストにしたくないため、`autoIncLast`は使用しません。

SourceCodeGeneratorのカスタマイズでよく見るコードと同様に、以下のようにoverrideします。

```scala
class CustomSourceCodeGenerator(model: Model) extends SourceCodeGenerator(model) {

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

  override def Table = new Table(_) {
    override def Column = new Column(_) {
      override def asOption: Boolean = autoInc
    }
  }
}
```

## 各拡張クラスを結合する

これでようやく準備ができました。 DB設定(url,user,password等)は各自の環境に合わせて下さい。

```scala 
  val db: DatabaseDef = Database.forURL(
    url = "jdbc:mysql://localhost/database?useSSL=false&nullNamePatternMatchesAll=true",
    driver = "com.mysql.cj.jdbc.Driver",
    user = "root",
    password = ""
  )

  val model: Model = Await.result(
    db.run(CustomMySQLProfile.createModel().withPinnedSession)
      .map(createModelWithoutSchema),
    Duration.Inf
  )

  new CustomSourceCodeGenerator(model).writeToFile(
    profile = "slick.jdbc.MySQLProfile",
    folder = "./output",
    pkg = "infrastructure.sample",
    container = "SampleTables",
    fileName = "SampleTables.scala"
  )
```

# 実行例

以下のテーブルからコードを自動生成してみます。

```mysql
CREATE TABLE `sample` (                                                                                 
  `id` int(11) NOT NULL AUTO_INCREMENT,                                                                 
  `default_null` timestamp NULL DEFAULT NULL,                                                           
  `default_current` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,                                       
  `default_current_on_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
  PRIMARY KEY (`id`)                                                                                    
) ENGINE=InnoDB DEFAULT CHARSET=utf8                                                                    
```

このテーブルを食わせて出力すると以下のようなコードが得られます。

```scala
package infrastructure.sample
// AUTO-GENERATED Slick data model
/** Stand-alone Slick data model for immediate use */
object SampleTables extends {
  val profile = slick.jdbc.MySQLProfile
} with SampleTables

/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait SampleTables {
  val profile: slick.jdbc.JdbcProfile
  import profile.api._
  import com.github.tototoshi.slick.MySQLJodaSupport._
  import org.joda.time._
  import slick.model.ForeignKeyAction
  // NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
  import slick.jdbc.{GetResult => GR}

  /** DDL for all tables. Call .create to execute. */
  lazy val schema: profile.SchemaDescription = Sample.schema
  @deprecated("Use .schema instead of .ddl", "3.0")
  def ddl = schema

  /** Entity class storing rows of table Sample
   *  @param id Database column id SqlType(INT), AutoInc, PrimaryKey
   *  @param defaultNull Database column default_null SqlType(TIMESTAMP)
   *  @param defaultCurrent Database column default_current SqlType(TIMESTAMP)
   *  @param defaultCurrentOnUpdate Database column default_current_on_update SqlType(TIMESTAMP) */
  final case class SampleRow(id: Option[Int] = None, defaultNull: Option[DateTime], defaultCurrent: Option[DateTime], defaultCurrentOnUpdate: Option[DateTime])
  /** GetResult implicit for fetching SampleRow objects using plain SQL queries */
  implicit def GetResultSampleRow(implicit e0: GR[Option[Int]], e1: GR[Option[DateTime]]): GR[SampleRow] = GR{
    prs => import prs._
    SampleRow.tupled((<<?[Int], <<?[DateTime], <<[Option[DateTime]], <<[Option[DateTime]]))
  }
  /** Table description of table sample. Objects of this class serve as prototypes for rows in queries. */
  class Sample(_tableTag: Tag) extends profile.api.Table[SampleRow](_tableTag, "sample") {
    def * = (Rep.Some(id), defaultNull, defaultCurrent, defaultCurrentOnUpdate) <> (SampleRow.tupled, SampleRow.unapply)
    /** Maps whole row to an option. Useful for outer joins. */
    def ? = (Rep.Some(id), defaultNull, Rep.Some(defaultCurrent), Rep.Some(defaultCurrentOnUpdate)).shaped.<>({r=>import r._; _1.map(_=> SampleRow.tupled((_1, _2, _3.get, _4.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    /** Database column id SqlType(INT), AutoInc, PrimaryKey */
    val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
    /** Database column default_null SqlType(TIMESTAMP) */
    val defaultNull: Rep[Option[DateTime]] = column[Option[DateTime]]("default_null")
    /** Database column default_current SqlType(TIMESTAMP) */
    val defaultCurrent: Rep[Option[DateTime]] = column[Option[DateTime]]("default_current")
    /** Database column default_current_on_update SqlType(TIMESTAMP) */
    val defaultCurrentOnUpdate: Rep[Option[DateTime]] = column[Option[DateTime]]("default_current_on_update")
  }
  /** Collection-like TableQuery object for table Sample */
  lazy val Sample = new TableQuery(tag => new Sample(tag))
}
```

この例では、`Sample`テーブルと`SampleRow`を使ってSlickのデータのやり取りが出来ます。

# その他
なお、このソースの修正版は、 https://github.com/peutes/slick-table-code-generator にあります。
環境ごとの修正は全て`application.conf`で設定できます。
`sbt run` を実行するとSlick Tableコードが自動生成できるため使って下さい。

以上です。これで幸せSlickライフを刻みましょう。