こんにちは、開発部のNishikawaです。
今日はScala と Play frameworkを使ってコードの自動生成を行なった時の話をまとめたいと思います。
どうしてこの記事を書こうと思ったか
Webアプリケーションの開発をしていると、よく本質じゃないところに時間がかかることがままあります。
例えば、データアクセス部分とか、コントローラの実装部分です。プログラムとしての責務がちゃんと別れれば別れるほど刺身たんぽぽ感が出てくるので、心を無にしてひたすらコピペとクラス名やメソッド名などの修正を行う人が多いと思います。
しかし、心を無にして釈迦のような志のもと作業を行なっていても所詮は人間なのでミスは起こります。そうすると、釈迦のような志から一変、鬼の形相でひたすらバグを潰しさらに時間がなくなるという無駄が無駄を産む状況になりがちです。
そういうのは一人で仕事を進める上ではとても無駄だし、本当に避けたい状況です。そこで目をつけたのはコードの自動生成です。
実際どういう作戦で何をしたのか
一括りにコードの自動生成と言っても、使うにはやはり一定の条件があります。今回Webアプリケーションを作成するにあたり以下のような構成のプログラムの実装を行いました。
実装はhtmlとjavascriptで行い、バックエンド側のAPIから情報を取得しレンダリングする。
REST APIの口を設けてView層からのリクエストを受け付ける
データへのアクセスはSlickを使用し、取得した値をプレゼンテーション層へ渡すためのモデルに移し替える
今回はコードを自動生成するにあたり以下のsbtプラグインとライブラリを使用しました。
https://github.com/unicredit/sbt-swagger-codegen
http://slick.lightbend.com/doc/3.2.0/code-generation.html
https://github.com/flyway/flyway-play
sbt-swagger-codegen
これは、sbtのプラグインでswagger specからPlay framework のコントローラとroutesファイル、json返却用のモデルを生成してくれます。
slick-codegen
これはデータベースにある実際のテーブル構造からSlickのコードを自動生成してくれます。
flyway-play
これはflywayのPlay framework用ライブラリです。これを使ってSQLを管理しました。
コード生成
前提
実際にコード生成を行う前に前提として、今回コード生成に使用したプロジェクトのディレクトリ構成を記載します。
example-web =============================================== > プロジェクトディレクトリ
├── app
│ ├── ErrorHandler.scala
│ ├── Module.scala
│ ├── com
│ │ └── astamuse
│ │ └── ・・・
│ └── infrastructure
│ └── models
│ └── postgresql
│ └── Tables.scala ============================== > 今回自動生成したSlickコード。
├── build.sbt
├── conf
│ ├── application.conf
│ ├── db
│ │ └── migration
│ │ └── default
│ │ ├── V1.0.0__example_database_web_app.sql == > flayway用のDDL その1。
│ │ └── V1.0.1__example_database_dummy_api.sql == > flayway用のDDL その2。
│ ├── logback.xml
│ └── routes ================================================ > 今回sbt-swagger-codegenで自動生成したroutesファイル。
├── docs
│ └── swagger
│ └── ExampleWebAPIController.yaml
├── project
│ ├── build.properties
│ ├── plugins.sbt
│ ├── project
│ │ └── target
│ └── target
├── target
│ ├── scala-2.12
│ │ ├── classes
│ │ ├── resolution-cache
│ │ ├── routes
│ │ ├── src_managed
│ │ │ └── main ========================================== > 自動生成されたコードが本来格納されるディレクトリ。今回sbt-swagger-codegenで自動生成したコントローラやモデルなどのコードが格納される。
│ │ │ └── swagger
│ │ │ └── codegen
│ │ │ ├── Model.scala
│ │ │ ├── controller
│ │ │ │ └── ExampleWebAPIController.scala
│ │ │ └── json
│ │ │ └── package.scala
│ │ └── test-classes
│ ├── streams
│ └── test-reports
└── test ====================================================== > テストディレクトリ
└── generator
└── slick
└── SlickCodeGenerator.scala ====================== > 今回はテストコードでslickのコードを生成するためのプログラムを書きました。
Slickコードの自動生成
ここからは、実際にSlickコードを自動生成する方法について述べます。
Slickのコード生成をするためにsbtに以下を書き込みライブラリを追加します。
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick-codegen" % "3.3.0"
,"org.flywaydb" %% "flyway-play" % "5.2.0"
)
追加したらプロジェクトディレクトリ配下の「conf/db/migration/default」にflywayのファイル命名規則に則りDDLを作成します。
作成したら以下のコードをテストディレクトリに作成します。
注意)今回は諸々省くためにテストコードに実装を行なっております。
package generator.slick
import org.flywaydb.core.Flyway
import org.scalatest.WordSpec
import play.api.Logger
import slick.jdbc.H2Profile.api._
import slick.codegen.SourceCodeGenerator
class SlickCodeGenerator extends WordSpec {
private val logger: Logger = Logger("verification")
"Generated code due to access to postgresql for slick." in {
logger.debug("Start slick verification.")
val slickProfile = "slick.jdbc.PostgresProfile"
val jdbcDriver = "org.postgresql.Driver"
val url = "jdbc:postgresql://localhost:5432/example_db"
val user = "admin"
val password = ""
val outputDirectory = "app"
val pkg = "infrastructure.models.postgresql"
val flyway: Flyway = Flyway.configure().dataSource(url , user, password).load()
flyway.migrate()
val args: Array[String] = Array( slickProfile, jdbcDriver, url, outputDirectory, pkg, user, password )
SourceCodeGenerator.main(args)
logger.debug("End slick verification.")
true === true
}
}
以上を記載したら、ローカル環境でPostgreSQLを起動し、コードに記載した内容でログインできることを確認したのちに実行します。
$ sbt "testOnly generator.slick.SlickCodeGenerator"
実行が完了したら、プロジェクトディレクトリの「app/infrastructure/models/postgresql」にコードが生成されていると思います。
Swagger Specからのroutesファイルおよびコントローラとモデルの自動生成
次はSwagger Specからroutesファイルとコントローラ、モデルを生成していきます。
sbtに以下のプラグインを追加します。
addSbtPlugin("eu.unicredit" % "sbt-swagger-codegen" % "0.0.11")
プラグインを追加したら、プロジェクトにプラグインを有効にし、
Play frameworkのディレクトリレイアウトのプラグインは無効にします。
lazy val `example-web` = (project in file("."))
.enablePlugins(PlayScala)
.disablePlugins(PlayLayoutPlugin)
.enablePlugins(SwaggerCodegenPlugin)
・・・
(中略)
・・・
scalaSource in Compile := baseDirectory.value / "app"
resourceDirectory in Compile := baseDirectory.value / "conf"
scalaSource in Test := baseDirectory.value / "test"
resourceDirectory in Test := baseDirectory.value / "conf"
swaggerCodeProvidedPackage := "com.astamuse.example"
swaggerSourcesDir := file("docs/swagger")
swaggerGenerateServer := true
swaggerModelCodeTargetDir := file("target/scala-2.12/src_managed/main")
swaggerServerCodeTargetDir := file("target/scala-2.12/src_managed/main")
以下を記載したら、プロジェクトディレクトリ配下に「docs/swagger」ディレクトリを作成し、そこにSwagger Specを配置します。
諸々の配置が完了したら以下のコマンドを実行します。
$ sbt swaggerRoutesCodeGen
$ sbt compile
コマンドの実行が完了すると、「target/scala-2.12/src_managed/main」配下にコードが作成されそれを含めた状態でアプリケーションをコンパイルしてくれます。
まとめると・・・
いかがslick-codegenとsbt-swagger-codegenを利用するために使った設定ファイルの内容です。
logLevel := Level.Warn
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.0")
addSbtPlugin("eu.unicredit" % "sbt-swagger-codegen" % "0.0.11")
import eu.unicredit.swagger.SwaggerCodegenPlugin
name := "example-web"
version := "1.0.0"
lazy val `example-web` = (project in file("."))
.enablePlugins(PlayScala)
.disablePlugins(PlayLayoutPlugin)
.enablePlugins(SwaggerCodegenPlugin)
resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
resolvers += "Akka Snapshot Repository" at "http://repo.akka.io/snapshots/"
scalaVersion := "2.12.2"
libraryDependencies ++= Seq(
ehcache
,ws
,guice
,"com.typesafe.play" %% "play-slick" % "4.0.0"
,"com.typesafe.slick" %% "slick-codegen" % "3.3.0"
,"org.flywaydb" %% "flyway-play" % "5.2.0"
,"org.postgresql" % "postgresql" % "42.2.5"
,"org.scalatestplus.play" %% "scalatestplus-play" % "4.0.0" % Test
,"com.h2database" % "h2" % "1.4.197" % Test
)
unmanagedResourceDirectories in Test += baseDirectory ( _ /"target/web/public/test" ).value
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "exclude", "SLICK_CODE_GENERATE")
scalaSource in Compile := baseDirectory.value / "app"
resourceDirectory in Compile := baseDirectory.value / "conf"
scalaSource in Test := baseDirectory.value / "test"
resourceDirectory in Test := baseDirectory.value / "conf"
swaggerCodeProvidedPackage := "com.astamuse.example"
swaggerSourcesDir := file("docs/swagger")
swaggerGenerateServer := true
swaggerModelCodeTargetDir := file("target/scala-2.12/src_managed/main")
swaggerServerCodeTargetDir := file("target/scala-2.12/src_managed/main")
以下に躓きやすいところを記載しておきます。
なぜPlay frameworkのディレクトリレイアウトのプラグインを無効にするのか。
Play frameworkのディレクトリレイアウトを利用するとコントローラのパッケージが「app/controllers」配下に固定されるので、コード生成したコントーラを読み込んでくれないためです。(もしかしたら他にやり方があるかもしれない)
なぜPostgreSQLを使ってコード生成するのか
h2を使って生成を試みたが、なぜかうまくいかなかったので泣く泣くPostgreSQLを利用しました。今後はh2で生成できるようにするつもりです。
今後に向けての課題
今回はSwaggerでの生成部分については「target/scala-2.12/src_managed/main」にコードを吐き出しましたが、Slickについてはそれができませんでした。
理由はh2を利用してのコード生成ができなかったためです。
なぜ、h2が使えないと「target/scala-2.12/src_managed/main」にコードが吐けないかというと、このディレクトリは毎回コンパイルのたびにコードを生成するためのものを配置するようにできております。そのため、何度コンパイルしてもコード生成を成功させてコードを配置しなければならないのですが、flywayを使っている以上、重複してDDLを実行するとエラーになります。そのため今回のようにコードの生成を「app」配下にし固定することでこの問題を回避しました。
今後はここの技術課題を克服してDAO部分も完全にコード管理しないようにしていきたいと思います。
まとめ
今回はコード生成に向けて色々と作り込みを行いました。この方法はあくまで少人数・・・というか個人で何かものを短期間で作らないといけない場合に有効だと思っております。
ピーキーな実装が求められるところについてはあまりお勧めできませんが、人数に関係なくコード実装量を減らしてくれる上、コードの管理量も減るため、試しに採用してみてはいかがでしょうか。
以上