この何ヶ月間くらい、ちょこちょことScala+Play2.4をいじってました。がっつり1日8時間とかいじるわけではなく、ほんとにちょこちょことですけども。
Play2では、GuiceによるDI(これがデフォルト)と、Compile-time DIという静的なDIの手法を選択できます。
このGuiceによるDI、というものについてはまた色々と意見があるようです(なんでScalaで型安全にできそうなのにGuice使わんとあかんの、とか)。
一応その辺は自由にできるようにしよう、としているらしいですが。
一応私が触っているScala + Play2.4は業務という区分なので、当然趣味だけで色々と選んでるわけではありません(半分くらいは・・・)。
Scalaである程度大きめのものを作ったことがなかったので、ついでにその辺の知見を集めるという目的もあります。
Play2とCompile-time DI
Play2でのCompile-time DIは、application.confに以下のように記載しなければいけません。
...
play.application.loader = BootstrapLoader
...
このBootstrapLoaderは、基本的には app/
直下に置いておくのが良さげです。
このBootstrapLoaderの指定を加えることで、PlayはGuiceによるDIをせず、このBootstrapLoaderから返されたapplicationを、 Play.current のようにして利用するようになります。
BootstrapLoaderは次のような感じになります。
import play.api.ApplicationLoader
import play.api.BuiltInComponents
import play.api.BuiltInComponentsFromContext
class BootstrapLoader extends ApplicationLoader {
def load(context: ApplicationLoader.Context) =
(new BuiltInComponentsFromContext(context) with BootstrapComponent).application
}
trait BootstrapComponent extends BuiltInComponents {
...
override lazy val router: Router = wire[Routes] withPrefix "/"
}
BootstrapComponentは、文字通りbootstrapするためのtraitですが、もし今までGuiceのDI(@Injectを付与する)を利用してwsとか使っていた場合、このBootstrapComponentにwithで付け加える感じになります。
import play.api.libs.ws.ning.NingWSComponents
trait BootstrapComponent extends BuiltInComponents with NingWSComponents {
import com.softwaremill.macwire.wire
...
override lazy val router: Router = wire[Routes] withPrefix "/"
}
ここでは、Routesを生成する際にmacwireを利用しています。Routeは非常に多くのControllerや他のRouterなどで構成されるのがおそらく常だと思いますので、普通に書いてたら無駄な労力になるので利用しています。
さて、Compile-time DI で最も重要な点としては以下の二点だと思います。以下の二点は私が開発していた中で実際にぶち当たったものでもあります。
- Play.currentは 絶対に 使わない
-- 使った瞬間どっかで落ちます。 - 依存はdef/lazy val で必ず記述する
2番目が若干イミフですが、必要になったときだけ取得する、ということができないと、macwireとかが活用できません。とくにlazy valは、こういった形のみでなく、Cake patternとかでも非常に重要な意味を持ったりします。
play-slickとCompile-time DI
Play2.4から、今までのanornではなく、Slick(しかもv3)がデフォルトになるという非常にクレイジーな変更が入りました。
幸いというかなんというか、私はSlickのv2の記憶がもうなかったんであれですが。
さて、PlayのSlickのページを見ても、Compile-time DIに対して言及しておらず、下手に書くと、起動はするけどDBへのアクセス時にNullPointer、みたいな悲しい現実を突きつけられることになります。というかなりました。
play-slickも、Compile-time DIのために、WSと同じようにtraitを用意しているので、それをBootstrapに混ぜてやれば、ひとまず準備はできます。
import play.api.libs.ws.ning.NingWSComponents
import play.api.db.slick.SlickComponents
trait BootstrapComponent extends BuiltInComponents with NingWSComponents
with SlickComponents {
import com.softwaremill.macwire.wire
...
override lazy val router: Router = wire[Routes] withPrefix "/"
}
これで準備は概ねできるんですが、DBアクセスについては一つ解決しておかないとならないものがありました。
ユニットテストを書きながら行っていますが、高レベルな部分についてはスタブを使うとしても、低レベルな部分はできればちゃんとDBにアクセスさせたいです。その辺を分離できるようにしておかないとなりません。
そこで、まずこんなものを作っておくことにします。
import java.io.File
import play.api.test.FakeApplication
import play.api.{ Application, Configuration }
// Play.currentを利用しないようにするためのStub
trait StubPlayAppComp extends PlayAppComp {
override lazy val playApp = Stub
}
// 毎回FakeApplicationを作成した場合、時間を浪費してしまうため、
// 一度だけ作成してそれを使い回す。ただし、テスト中にPlay.startなどは実行してはならない。
object Stub extends PlayApp {
private var current: Application = null
override implicit def app = {
if (current == null) {
// application.confとかを読ませる方法がわからんのでここで直接記載。読み出し方がわかるんであればそれをやればOK
val config = Map(
...
)
current = new FakeApplication(additionalConfiguration = config)
}
current
}
override def isProd = false
override def configuration = Configuration(getConfig("conf/application-test.conf"))
override def getFile(path: String) = new File(path)
}
このStubを、実際にApplicationが必要な場所にmixinします。例えば、テスト用DBを毎回クリーンアップするためのtraitはこんな感じのものを用意しています。
import play.api.db.slick.DatabaseConfigProvider
import slick.driver.JdbcProfile
import slick.driver.MySQLDriver.api._
// SlickComponentは、getDBをdefしているだけのtrait
trait MockSlickComponent extends SlickComponent with StubPlayAppComp {
override lazy val connector = new DBConnector {
override def getDB: Database = {
DatabaseConfigProvider.get[JdbcProfile]("test")(playApp.app).db
}
}
}
trait AutoRollback extends Scope with After with MockSlickComponent {
def fixture(implicit app: Application) {}
// わざわざこんなことをしている理由は、Slick のV3から、autocommitの制御がやたらわかりづらくなったため。下手に色々やるよりは、シンプルにやってしまった方が早い
private def resetDatabase = {
val db = connector.getDB
val actions = (for {
ts <- db.run(SimpleDBIO({ con =>
val ts = con.connection.getMetaData.getTables(null, null, null, null)
var tables = List[String]()
while (ts.next()) {
tables = tables ++ List(ts.getString(3))
}
tables.filter(!_.contains("schema_version"))
}))
_ <- db.run(DBIO.sequence(ts.map(tab => sqlu"delete from #${tab}")))
} yield ())
Await.result(actions, Duration("10s"))
}
def withRollback[T](app: Application)(t: => T) = {
fixture(app)
t
}
final override def after: Unit = {
resetDatabase
}
}
これによって、実際にテストの中で利用するものとしては、StubPlayAppCompのappをstartさせた中で、
"..." should {
"..." in new AutoRollback {
override def fixture(implicit app:Application) {
... // 色々
}
withRollback(app) {
... //fixtureの内容が設定された後に実行するテスト
}
}
}
のようにかけます。もっとシンプルにしたかったんですが、とりあえずこれで妥協してます。
こういったものの他に、他のComponentやDBアクセスを行う層などにmacwireなどでCompile-timeで解決するようにする、というのもありますが、それはそれで。
Compile-time DI vs Guice DI
正直、今のplay-*系統、特にサードパーティのものは、Guice DIにのみ対応しているもの、というのも多いです。
また、Guice DIでなければModuleを使うことができません。正確に言えば使えますが、対応しているものも少ない印象です。
対して、Compile-time DIについては、基本的にはScalaの基本機能(lazy valやtrait)をフル活用することで成り立っているため、やはりちゃんと通っていれば、NullPointerなどはそうそう発生しません。
ただ、どうしても記述量は圧倒的に多くなります。Bootstrapなどは、本来自分で書かなくてもいいものですが、Routeまで含めて自分で書いてやらないといけないのはそれなりに面倒くさいです。
また、Compile-time DIは、コンパイルにめちゃめちゃ時間がかかるようになります。
200ファイルくらいで20sくらいですが(論理8コア、Broadwell世代のCore i7)、依存性が複雑なものであればどんどん時間がかかるようになります。
これを回避する手段は基本的にないので、Scalaのコンパイル速度の向上に期待するか、いいマシンを買ってもらいましょう。
総括
Scalaをこれだけ書いたのは初めてですが、それなりにGoogle先生に伺えばでてきます。
今回の記事も基本的にはStackOverflowとかGithubのissueとか調べた結果です。
ですが、この辺は最初に整備されて後は触らぬ神に祟りなし状態になりがちなので、メモとして残しておくことには意義があると思い・・・
追記:
どうでもいいんですが、Scalaってメソッドの引数の型省略できないのかな・・・OCamlばっかりやっててScalaに来るとやたらと面倒臭く見えますな・・・