Play 2.4のサブプロジェクト

初めまして!アリクイです。イギリス人ですけど、日本の文化に興味を持って3年前日本に来て、鳥取県で英語を教えるかたわら日本語を勉強しました。鳥取でエンジニアの仕事は少ないので東京に引っ越して、今年の5月からナイルで働いてます。宜しくお願いします!

たまに二つのプロジェクトでコードを共有したいです。別々でコンパイルしたいし、別のサーバーでデプロイしたいし、同じプロジェクト中で混ぜたくないです。例えば、

  • 二つのプロジェクトは同じデータベースを使ってます。DAOは2回作りたくない時
  • 管理画面は本サイトと同じモデルとヘルパーを使いたいですが、別の認証があるサーバーでデプロイしたい時
  • 大きいプロジェクトで独自なライブラリーを別のところでまとめたい時

Play 2.4でこの問題を解決したい時、サブプロジェクトが使えます。サブプロジェクトを”subprojects”のフォルダーに入れたら、build.sbtのファイルで依存を定義できます。簡単なブログアプリのために管理画面のサブプロジェクトを作成する例を見ましょう。

この説明のコードは私のGithubのアカウントに入ってます。完成されたサブプロジェクトは「subprojects-example」というブランチに入ってます。

blog_post_image

このアプリはブログポストを見ることができます。アカウントを登録してログインすれば、投稿できます。

サブプロジェクトを作成

Play 2.4のサブプロジェクトは「subprojects」というフォルダーに置きます。管理画面のために二つのサブプロジェクトを作ります。commonは共有するデータを置く場所です。そして、adminというサブプロジェクトに管理画面のファイルを置きます。とりあえず、commonを作って、build.sbtファイルを作ります。commonという名前をつけたら、ルートのアプリケーションから

build_sbt

内容はルートプロジェクトに似ていますけど、commonは独自で走らないようにproject in fileは書かなくてもいいです。

name := """common"""
version := "1.0-SNAPSHOT"
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
cache,
ws,
"com.typesafe.play" %% "play-slick" % "1.0.0",
"mysql" % "mysql-connector-java" % "5.1.24"
)
resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"
// Scala Compiler Options
scalacOptions in ThisBuild ++= Seq(
"-target:jvm-1.8",
"-encoding", "UTF-8",
"-deprecation", // warning and location for usages of deprecated APIs
"-feature", // warning and location for usages of features that should be imported explicitly
"-unchecked", // additional warnings where generated code depends on assumptions
"-Xlint", // recommended additional warnings
"-Xcheckinit", // runtime error when a val is not initialized due to trait hierarchies (instead of NPE somewhere else)
"-Ywarn-adapted-args", // Warn if an argument list is modified to match the receiver
"-Ywarn-inaccessible",
"-Ywarn-dead-code"
)
// Play provides two styles of routers, one expects its actions to be injected, the
// other, legacy style, accesses its actions statically.
routesGenerator := InjectedRoutesGenerator
自分の好きなデータベースとコンパイラー設定はもちろん変更しても大丈夫です。
ルートプロジェクトのbuild.sbtに
lazy val root = (project in file("."))
.enablePlugins(PlayScala)
.dependsOn(common)
.aggregate(common)
lazy val common = (project in file("subprojects/common"))
.enablePlugins(PlayScala)
を追加しましょう。activator compileを流したら、こんな感じになります、、、
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}common...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}root...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 19 Scala sources and 1 Java source to /Users/neve_ol/development/scala/simple_blog2/target/scala-2.11/classes...
[success] Total time: 27 s, completed 2015/12/10 14:42:44
commonとrootは両方コンパイルされたので、成功でした。Playがちゃんと動くように、project/pluginsのファイルも追加しましょう。
sbt_plugins
pluginsはsbt-pluginが大事ですが、ルートプロジェクトと同じ内容で大丈夫です。
同じ感じでadminを設定しましょう。adminはrootと一緒にコンパイルしないので、rootみたいにdependsOn(common)を書きます。plugins.sbtはcommonと同じ感じで設定します。
lazy val common = (project in file("../common"))
.enablePlugins(PlayScala)
lazy val admin = (project in file("."))
.enablePlugins(PlayScala)
.dependsOn(common)
.aggregate(common)
ルートのbuild.sbtでサブプロジェクトを定義しなきゃいけないです。
lazy val admin = (project in file("subprojects/admin"))
.enablePlugins(PlayScala)
.dependsOn(common)
.aggregate(common)
activatorのコンソルから「project [name]」を使えば、プロジェクトが変えることができます。clean compileをする時、rootはadminのプロジェクトをコンパイルしないように確認しましょう。
[simple_blog2] $ compile
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}common...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}root...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 19 Scala sources and 1 Java source to /Users/neve_ol/development/scala/simple_blog2/target/scala-2.11/classes...
[success] Total time: 15 s, completed 2015/12/10 15:14:25
[simple_blog2] $ project admin
[info] Set current project to admin (in build file:/Users/neve_ol/development/scala/simple_blog2/)
[admin] $ clean
[success] Total time: 0 s, completed 2015/12/10 15:14:43
[admin] $ compile
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}common...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Updating {file:/Users/neve_ol/development/scala/simple_blog2/}admin...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[success] Total time: 5 s, completed 2015/12/10 15:14:49

commonにコードを移動

次の作業は共有したいファイルをcommonのプロジェクトに移動することです。package名は元々の名前プラスサブプロジェクト名は普通です。例えば、modelsはmodels.commonになります。importを全部更新するのは辛いです、、、
move_classes
ファイルだけじゃなくて、ルートもアセットも共有したい時があります。こうするために、common/conf/common.routesのファイルを作りましょう。下の内容を入れてみましょう。
GET /assets/*file controllers.common.Assets.versioned(path="/public", file: Asset)
rootのプロジェクトのroutesファイルにこの内容も追加
-> /         common.Routes
今の設定で、アセットをcommon/publicに移動すれば表示されないです。なぜかというと、controllers.common.Assetsというコントローラーは存在していないです。それに、commonはrootの前にコンパイルされて、controllers.Assetsの存在について知らないです。解説するために、common/app/controllersのなかでcontrollers.common.Assetsを作りましょう。PlayのAssetsBuilderをextendすれば済みます。
package controllers.common
import play.api.http.DefaultHttpErrorHandler
class Assets extends controllers.AssetsBuilder(DefaultHttpErrorHandler)

イメージとスプレッドシートをcommonに移動すれば、ちゃんと表示されます。これを定義してから、app/assets/stylesheets/main.cssをsubprojects/common/app/assets/stylesheets/main.cssに移動できました。
stylesheets
管理画面を作成
最後に、adminの環境を設定しましょう。adminは独自でコンパイルしたいので、commonと比べてもっと必要です。とりあえず、admin/conf/application.confを作りましょう。だいたいrootのapplication.confの内容と同じで大丈夫ですが、ルートのファイルはディファウルトじゃないです。
play.http.router=admin.Routes
上のコンフィグを追加する必要があります。ルートファイルも変更しましょう。例として、ユーザーを全部見る画面を作りたいです。admin/conf/admin.routesのなかでこの内容を追加します。
GET     /users                      admin.controllers.UserManagementController.login
そして普通にrootプロジェクトみたいにadminをpackage名に追加すれば普通に開発できます。
controllers/userManagementController.scala
package admin.controllers
import play.api._
import play.api.mvc._
import dao.common._
import models.common.User
import scala.concurrent.ExecutionContext.Implicits.global
class UserManagementController extends Controller {
def index = Action.async { implicit request =>
dao.common.UserReadDAO.getAll.map { users =>
Ok(views.html.users.index(users))
}
}
}

views/layouts/admin.scala.html
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
<title>@title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" media="screen" href="@controllers.common.routes.Assets.versioned("stylesheets/main.css")">
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">
Admin
</a>
</div>
</div>
</nav>
<div class="container">
@content
</div>
</body>
</html>

スタイルシートのルートははっきりと見てみてください。routes.Assets.versionedだけじゃなくて、コントローラー名も書かなきゃいけないです。
views/users/index.scala.html
@import models.common.User
@(users: Seq[User])
@layouts.admin("Users") {
<h1>Users</h1>
<ul>
@for(user <- users) {
<li>@user.username</li>
}
</ul>
}

画面を見たければ、activatorを起動してから、前みたいにproject adminを使ってadminプロジェクトを選択します。そして普通にrunで起動できます。localhost:9000/usersに行けば、ユーザーの画面が見れます。
users
このアプリは「James」と呼ばれるユーザーが二人いるみたいです ^^;
管理画面ログイン機能がまだないし、ユーザーの編集機能は実装してないし、いろんな仕事はまだやらなきゃいけないです。ただ、これを読んだら簡単なPlay 2.4のサブプロジェクト設定がわかるようになったと祈ってます。
コード:https://github.com/jamesneve/simple_blog2/tree/subprojects-example