DDDのリポジトリを実装する際、ほとんどのケースでDAOが必要になります。が、ボイラープレートが多く、自動生成したいところです。というわけで作りました。
どうやって自動化するか
指定されたスキーマのJDBCメタ情報とテンプレートをマージさせて、ソースコードを出力します。その機能を提供するのがsbt-dao-generator1です。つまり、sbt
からコマンド一発でこういうことができるようになるわけですが、 DBのインスタンスが立ち上がっていて、スキーマ情報が組み込まれた状態でないと使えません。
sbt
から組み込みMySQLを起動するプラグインです。MySQL固定です…。
こちらは言わずもがな、有名なsbtプラグイン。組み込みMySQL上にスキーマを自動作成するために使います。
環境構築手順
実際のサンプルコードは、j5ik2o/scala-ddd-baseをみてください。
flyway
を扱うプロジェクトflywayとDAOを自動生成するプロジェクトexampleはわけています。
project/plugins.sbt
プラグインを追加しましょう
addSbtPlugin("com.chatwork" % "sbt-wix-embedded-mysql" % "1.0.9")
addSbtPlugin("jp.co.septeni-original" % "sbt-dao-generator" % "1.0.8")
addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "5.0.0")
テンプレートを作りましょう
FTLでDAOのテンプレートを書きます。以下はSkinnyORMのための例です。レコードクラスとDAOクラスです。
case class ${className}Record(
<#list primaryKeys as primaryKey>
${primaryKey.propertyName}: ${primaryKey.propertyTypeName}<#if primaryKey_has_next>,</#if></#list><#if primaryKeys?has_content>,</#if>
<#list columns as column>
<#if column.columnName == "status">
<#assign softDelete=true>
</#if>
<#if column.nullable> ${column.propertyName}: Option[${column.propertyTypeName}]<#if column_has_next>,</#if>
<#else> ${column.propertyName}: ${column.propertyTypeName}<#if column_has_next>,</#if>
</#if>
</#list>
) extends Record
object ${className}Dao extends Dao[${className}Record] {
override def useAutoIncrementPrimaryKey: Boolean = false
override val tableName: String = "${tableName}"
override protected def toNamedValues(record: ${className}Record): Seq[(Symbol, Any)] = Seq(
<#list columns as column> '${column.name} -> record.${column.propertyName}<#if column.name?ends_with("id") || column.name?ends_with("Id")>.value</#if><#if column_has_next>,</#if>
</#list>
)
override def defaultAlias: Alias[UserAccountRecord] = createAlias("${className[0]?lower_case}")
override def extract(rs: WrappedResultSet, s: ResultName[${className}Record]): ${className}Record = autoConstruct(rs, s)
}
特定のDAOに依存しないので、ほとんどのものに対応できるはず。以下は、Slick用とSkinny用の両方に対応したテンプレート例です。どちらでも好きなORMを使ってください。
https://github.com/j5ik2o/scala-ddd-base/blob/reboot/example/templates/template.ftl
テンプレートの書き方はこちら参照。カラム名をあらかじめプロパティ名としてテンプレートコンテキストに含めているので、簡単に書けるはずです。
build.sbt
https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L114-L136
このプロジェクトではchatwork/sbt-wix-embedded-mysqlとflyway/flyway-sbtを使って自動的にスキーマを作ります。flywayMigrate := (flywayMigrate dependsOn wixMySQLStart).value
としているので、sbt flyway/flywayMigrate
する前に組み込みMySQLが起動します。
https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L138-L188
JDBCの接続先設定は、flyway
プロジェクトと同じ設定を指定してください。
このプロジェクトでは、septeni-original/sbt-dao-generatorを使ってJDBCメタ情報とテンプレートをマージして、DAOクラスのソースコードを生成します。
今回は生成物をGitで管理したかったので、以下のようにして通常のソースコードと同じパスに出力していますが、(sourceManaged in Compile).value
を使ってtarget/src_managed
に出力することも可能です。
outputDirectoryMapper in generator := {
case s if s.endsWith("Spec") => (sourceDirectory in Test).value
case s => new java.io.File((scalaSource in Compile).value, "/com/github/j5ik2o/dddbase/example/dao")
},
outputDirectoryMapper in generator := { className: String => (sourceManaged in Compile).value },
コンパイル時にソースコード生成するには以下のようにしてください。コンパイルと無関係に生成タスクを実行したい場合はsbt generator::generateAll
としてください。
sourceGenerators in Compile += (generateAll in generator).value
コンパイルより前に出力したい場合は以下でも動作します。
compile in Compile := ((compile in Compile) dependsOn (generateAll in generator)).value
あとでexample
プロジェクトからflyway
プロジェクトに依存することをお忘れ無く
val example = ...
dependsOn(..., flyway)
生成
コンパイル時に自動的に以下が行われます。
- 組み込みMySQLの起動
- flyway マイグレーション実行
- JDBCメタ情報とテンプレートのマージとソースファイル出力
- コンパイル
実際に生成されたソースコードはこちら。
https://github.com/j5ik2o/scala-ddd-base/blob/master/example/src/main/scala/com/github/j5ik2o/dddbase/example/dao/UserAccount.scala
package com.github.j5ik2o.dddbase.example.dao
package slick {
import com.github.j5ik2o.dddbase.slick.SlickDaoSupport
trait UserAccountComponent extends SlickDaoSupport {
import profile.api._
case class UserAccountRecord(
id: Long,
status: String,
email: String,
password: String,
firstName: String,
lastName: String,
createdAt: java.time.ZonedDateTime,
updatedAt: Option[java.time.ZonedDateTime]
) extends SoftDeletableRecord
case class UserAccounts(tag: Tag)
extends TableBase[UserAccountRecord](tag, "user_account")
with SoftDeletableTableSupport[UserAccountRecord] {
def status = column[String]("status")
def email = column[String]("email")
def password = column[String]("password")
def firstName = column[String]("first_name")
def lastName = column[String]("last_name")
def createdAt = column[java.time.ZonedDateTime]("created_at")
def updatedAt = column[Option[java.time.ZonedDateTime]]("updated_at")
override def * =
(id, status, email, password, firstName, lastName, createdAt, updatedAt) <> (UserAccountRecord.tupled, UserAccountRecord.unapply)
}
object UserAccountDao extends TableQuery(UserAccounts)
}
}
package skinny {
import com.github.j5ik2o.dddbase.skinny.SkinnyDaoSupport
import scalikejdbc._
import _root_.skinny.orm._
trait UserAccountComponent extends SkinnyDaoSupport {
case class UserAccountRecord(
id: Long,
status: String,
email: String,
password: String,
firstName: String,
lastName: String,
createdAt: java.time.ZonedDateTime,
updatedAt: Option[java.time.ZonedDateTime]
) extends Record
object UserAccountDao extends Dao[UserAccountRecord] {
override def useAutoIncrementPrimaryKey: Boolean = false
override val tableName: String = "user_account"
override protected def toNamedValues(record: UserAccountRecord): Seq[(Symbol, Any)] = Seq(
'status -> record.status,
'email -> record.email,
'password -> record.password,
'first_name -> record.firstName,
'last_name -> record.lastName,
'created_at -> record.createdAt,
'updated_at -> record.updatedAt
)
override def defaultAlias: Alias[UserAccountRecord] = createAlias("u")
override def extract(rs: WrappedResultSet, s: ResultName[UserAccountRecord]): UserAccountRecord =
autoConstruct(rs, s)
}
}
}
まとめ
このプロジェクト構成は、仕事でも結構がっつり使っていて気に入っています。スキーマ変更が起こっても、DAOは一瞬で自動生成できるので楽になると思います。興味あれば使ってみてください!