読者です 読者をやめる 読者になる 読者になる

謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

Playframework2.5.x でプロジェクト分離してみた

サブプロジェクト自体は、Playframework2.3 から存在していて、当時はあまり使い道も思い浮かばなかったが、今では大分使いやすくなってたのでメモ。

完全独立した sbt プロジェクトを取り込む

このケースでは、完全に独立した sbt モジュールを取り込みます。
用途的には、コンソールプログラムとWebアプリでロジックを共有するために切り離すとか、そういう用途だ。

因みに、それ自体が独立してコンパイルできる sbt プロジェクトであればいいので、おそらく、Playframework アプリケーションでもいけると思う。
ただし、個人的にはそれは失敗した。(設定も面倒だし、後述の「Play サブプロジェクトを作成する」を参照した方がきっといい)

下記の例は単純に sbt のライブラリを取り込む設定だ。
ディレクトリ構成的には下記。

  • project_root
    • build.sbt
    • modules
      • common_lib
        • build.sbt

common_lib がsbt プロジェクトだ。

project_root/build.sbt を下記のようにする。

// ① common_lib という形で、サブプロジェクトがあることを宣言
lazy val common_lib = project in file("modules/common_lib")

lazy val root =
  (project in file("."))
    .enablePlugins(PlayScala)
    .dependsOn(common_lib) // ② common_lib に依存しているという

root 定義に dependsOn で common_lib に依存していることを表す。
これだけなので、とりあえずこれで、common_lib 内のコードを取り込んだ側で利用可能になる。

Play サブプロジェクトを作成する

プロジェクトを機能ごとに分離したい場合に使用。
コンパイルはプロジェクト単位で行うので、コンパイル時間をどうにかしたい場合(変更のあったプロジェクトと、その依存プロジェクトのみ差分コンパイルが走る)や、機能間依存を極力減らしつつ機能の充実化をする(要するにプロジェクト整理)ために使用する。

ディレクトリ配置は下記のようにしてみた。

  • project_root
    • build.sbt
    • app : アプリケーションソース
    • modules
      • view_template
        • build.sbt
        • app : 共有ソース

project_root/build.sbt の側はこれでOK

lazy val viewTemplate =
  (project in file("modules/view_template"))
    .enablePlugins(PlayScala)

lazy val root =
  (project in file("."))
    .enablePlugins(PlayScala)
    .dependsOn(viewTemplate)
    .aggregate(viewTemplate)

enablePlugins(PlayScala) を食わせると、サブプロジェクト側も Play のプロジェクトとして扱われるようになる。

次に、 modules/view_template/build.sbt 側の中身は下記。
注意が必要なのは、scalaVersion を本体に合わせないとコケる。

name := "view_template"

organization := "net.white-azalea"

version := "0.1-SNAPSHOT"

scalaVersion := "2.11.7"

libraryDependencies ++= Seq(
  jdbc,
  evolutions,
  cache,
  ws,
  filters,
  specs2 % Test,

  /* database definition */
  "com.h2database" % "h2" % "1.4.192"
)

この時、サブプロジェクト側のディレクトリ構成もは、Playframewrok アプリ同様のディレクトリ構成になる。

Controller だけでなく、URL も部分切り出しできるので、それをするなら routes 分離(公式ドキュメント) 見ればいいと思う。

サブプロジェクトの記述を簡単にしたい

中身を考えればわかることだが、organization などは明らかに共通だし、サブプロジェクトごとに書くのはめんどい。

  • project_root
    • build.sbt
    • project
      • Common.scala ← 追加
    • app : アプリケーションソース
    • modules
      • view_template
        • build.sbt
        • app : 共有ソース

Common.scala には下記を記述。
これはサブプロジェクトにも呼び出せる設定だ。

import sbt._
import Keys._
import play.sbt.PlayImport._

object Common {

  val settings = Seq(
    organization := "net.white-azalea",
    version := "0.1-SNAPSHOT",
    scalaVersion := "2.11.7"
  )

  val dependencies = Seq(
    jdbc,
    evolutions,
    cache,
    ws,
    filters,
    specs2 % Test,

    /* database definition */
    "com.h2database" % "h2" % "1.4.192"
  )
}

これを記述すると、project_root/build.sbt なんかは下記のようになる。

name := """project_root"""

Common.settings

lazy val viewTemplate =
  (project in file("modules/view_template"))
    .enablePlugins(PlayScala)

lazy val root =
  (project in file("."))
    .enablePlugins(PlayScala)
    .dependsOn(viewTemplate)
    .aggregate(viewTemplate)

libraryDependencies ++= Seq(
  /* なんか追加で欲しいやつ */
  "net.white-azalea" %% "simpleplaymodules" % "0.1.3"
) ++ Common.dependencies

pipelineStages := Seq(rjs)

TwirlKeys.templateImports += "views.html.commons._"

当然、これはサブプロジェクトでも使用可能なので、 modules/view_template/build.sbt

name := "view_template"

Common.settings

libraryDependencies ++= Common.dependencies

というように省略できる。

サブプロジェクトのテスト

サブプロジェクトに処理を分けるなら、当然の如くテストもそれぞれに分離したい筈。
しかし、ここで気をつけなければならない点は サブプロジェクトに書いたテストは、サブプロジェクト内の application.conf を参照する ということだ。

例えば上記の例で、言えばこういう事だ。

  • project_root
    • build.sbt
    • conf
      • application.conf : run や dist 時、project_root 管轄のテストで参照
    • modules
      • view_template
        • build.sbt
        • conf
          • application.conf : view_template 配下のテストで参照

サブプロジェクトである程度完結すべきという意味では全く間違いではない。
しかし、Play の設定を複数書かなきゃいけないなんてナンセンスだ。

そこで、テスト実行時のみ、親プロジェクトの project_root/conf/application.conf を参照するようにしてしまう。
やり方は簡単で、project_root/project/Common.scala に追記してやる。

object Common {

  val settings = Seq(
    organization := "net.white-azalea",
    version := "0.1-SNAPSHOT",
    scalaVersion := "2.11.8",
    javaOptions in Test ++= Seq("-Dconfig.file=../../conf/application.conf") // ← 追加
  )

上記までの手順を踏んでいれば割と楽な筈だ。

ちなみにこのままだと、project_root 管轄のテストにも影響が出るので、project_root/build.sbt だけは上記設定を上書きするか削除するといい。

サブプロジェクト間で、更に依存を持たせてみる

common_libview_template を作ったのだから、当然他から参照したい。
そこで、例えばログインプロジェクトを Play サブプロジェクトとして作成する。

lazy val common_lib =
  project in file("modules/common_lib")

lazy val viewTemplate =
  (project in file("modules/view_template"))
    .enablePlugins(PlayScala)

// サインインロジックや URL を定義すると仮定。
lazy val signIn =
  (project in file("modules/sign_in"))
    .enablePlugins(PlayScala)

lazy val root =
  (project in file("."))
    .enablePlugins(PlayScala)
    .dependsOn(common_lib, viewTemplate, signIn) // 依存に追加
    .aggregate(common_lib, viewTemplate, signIn) // 集約に追加

ここまでは普通のプロジェクト取り込みと変わらない。
signIncommon_libviewTemplate を使用(依存)するならそう書くといい。

lazy val signIn =
  (project in file("modules/sign_in"))
    .enablePlugins(PlayScala)
    .dependsOn(common_lib, viewTemplate) // 追記

これで、signIn プロジェクトから common_lib 等のソースを参照できるようになる。