最近はGoやScalaでサービスを開発したり、既存のシステムを置き換えたりする事例が増えてきてますよね。私も仕事ではruby/railsがメインなのですが、新しくアプリケーションを開発するにあたって別の言語・フレームワークを検討する機会があり、少しだけScala(とJava)のフレームワークPlayを触ったので、**Railsでいう◯◯はPlayではどうやるの?**という点をまとめたいと思います。
ちなみに、この記事を書いてる時点で筆者のScala歴・Play歴は10時間くらいですので、「Play Framework入門」と銘打ったものの、「Play Framework紹介」に近いかもしれませんwご了承ください。
サンプルコード
以下ではこちらのコードから色々抜粋して貼り付けてます。
https://github.com/suzan2go/hello_play
公式のScalaToDoListチュートリアル(2.2.Xと少し古いのですが…最新のドキュメントではチュートリアルが見つからず…)を参考に、play 2.4.4で動くように色々試行錯誤したToDoListのアプリです。
Play Frameworkを始める
JDK 1.8のインストール
playの2015/12/8時点の最新版2.4.4
を動かすには、JDK 1.8
が必要です。Macの場合は以下からDLするか、brew cask
でインストールできます。
Java SE - Downloads
brew cask install java
Playのインストール
PlayをDLしましょう。以下のURLからDLできます。
https://www.playframework.com/download
Macの場合はbrew
でインストールできます。
brew install typesafe-activator
手順を調べるとbrew install play
でインストールとなっているものがありますが、2.3以降はbrew install typesafe-activator
でインストールとなったようですので注意。
新しくアプリを作る(= rails new
)
以下のコマンドで新しくアプリの開発を始められます。
activator new my-first-app play-scala
以下の様にファイルが作成されます。デフォルトではmodel
というディレクトリは作成されないので、自分で作っていく感じになります。というかこの辺りのフォルダの命名規約はあまり無いようで、playで作られたアプリをgithubで眺めていると全然こういう名前の配置になってなかったりします。(ちなみにplay-scala
の部分をplay-java
に変えると、java
用のテンプレート
が作成されます。)
├── LICENSE
├── README
├── activator
├── activator-launch-1.3.6.jar
├── app
│ ├── controllers
│ └── views
├── bin
├── build.sbt
├── conf
│ ├── application.conf
│ ├── evolutions
│ ├── logback.xml
│ └── routes
├── logs
│ └── application.log
├── my-first-scala.iml
├── project
│ ├── build.properties
│ ├── play-fork-run.sbt
│ ├── plugins.sbt
│ ├── project
│ ├── sbt-ui.sbt
│ └── target
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
├── target
│ ├── native_libraries
│ ├── resolution-cache
│ ├── scala-2.11
│ ├── streams
│ └── web
└── test
├── ApplicationSpec.scala
└── IntegrationSpec.scala
またコマンドラインから、activator ui
とすると、Webベースの開発環境を立ち上げることができます。ちょっと試しに使うくらいであれば、IDEを準備する時間も勿体無いなので、こちらを使っても良いかもしれません。
接続するデータベースの情報を書く(=config/database.yml
)
conf/application.conf
に書きます。
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.username=sa
db.default.password=""
データベースのmigrationを書く(= db/migrate/...
)
playにはevolutionという仕組みがあり、conf/evolutions/default/..
というディレクトリの下に、1.sql, 2.sql
という形でmigrationファイルを書いていく形になります。
# Tasks schema
# --- !Ups
CREATE SEQUENCE task_id_seq;
CREATE TABLE task (
id integer NOT NULL DEFAULT nextval('task_id_seq'),
label varchar(255)
);
# --- !Downs
DROP TABLE task;
DROP SEQUENCE task_id_seq;
この状態でアプリケーションにアクセスすると以下のような画面になります。ここでapply this script now
ボタンをクリックすると、データベースにmigrationが反映されます。
外部ライブラリを導入する(= Gemfile )
sbtというツールを使って管理するようです。アプリケーションrootに配置されたbuild.sbt
にはslick
などアプリケーションで実際に使用するライブラリを、project/plugins.sbt
にはcoffee script
やless
のコンパイルを行うためのものを記述するっぽい。
├── build.sbt
├── project
│ ├── build.properties
│ ├── play-fork-run.sbt
│ ├── plugins.sbt
│ ├── project
│ ├── sbt-ui.sbt
│ └── target
サンプルコードでは以下のように、bootstrap
をbuild.sbt
に書いています。
"org.webjars" %% "webjars-play" % "2.4.0-1",
"org.webjars" % "bootstrap" % "3.1.1-2"
これはwebjarといって、CSSやJSのライブラリをJAR
ファイルにまとめたもので、こちらのサイトに公開されています(webjar)。rails-assetsみたいなもんですかね。昨今のフロントエンド事情からするに、フロントエンドはこちらを使わずにnpm等で管理するのが良い気もしますがどうなんでしょうね。
webjarを使う場合にはルーティングに以下を追加します。
GET /webjars/*file controllers.WebJarAssets.at(file)
ルーティングを書く(= config/routes.rb
)
conf
ファイルの下にroutes
というファイルがありますね。ここにルーティングを書いていきます。
├── conf
│ ├── application.conf
│ ├── evolutions
│ ├── logback.xml
│ └── routes
実際にどう書いていくかというと、以下の様な感じになります。
railsでいう、get 'products/:id' => 'catalog#view
のように、ルーティングに対して対応するコントローラとアクションを指定していきます。
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.Application.index
GET /tasks controllers.Application.tasks
POST /tasks controllers.Application.newTask
POST /tasks/:id/delete controllers.Application.deleteTask(id: Long)
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
GET /webjars/*file controllers.WebJarAssets.at(file)
上記のサンプルではApplicationコントローラに全て書いてしまっていますが、実際の開発では例えばTaskのコントローラは分けて書くらしいです。
controllerを書く
controllers
配下のApplicaton.scala
に、先ほど書いたroutesに
対応するアクションを書いていきます。
├── app
│ ├── controllers
│ │ └── Application.scala
見れば大体想像付くと思いますが、以下の様な処理になってます。
- indexでは、
tasks
にリダイレクト - tasksでは、
index.html.scala
のテンプレートに対して、全てのタスクと、taskFormを渡しています。
※ taskFormについては後述
def index = Action {
Redirect(routes.Application.tasks)
}
val taskForm = Form(mapping(
"id" -> ignored(0: Long),
"label" -> nonEmptyText,
"body" -> nonEmptyText)(Task.apply)(Task.unapply))
def tasks = Action {
Ok(views.html.index(Task.all(), taskForm))
}
modelを書く
デフォルトではapp
配下にmodels
というディレクトリはないので、自分でつくります。
├── app
│ ├── controllers
│ │ └── Application.scala
│ ├── models
│ │ └── Task.scala
以下では普通にコードの中でSQL書いてますが、これはanorm
というライブラリをつかっているためです(play 2.3まではこちらがデフォルト)。play2.4からはslick
というORMがデフォルトらしく、SQLを直書きしなくても大丈夫なようです。(本当はslickで書き直したかったのですが、力尽きました)
package models
import anorm._
import anorm.SqlParser._
import play.api.db._
import play.api.Play.current
case class Task(id: Long, label: String, body: String)
object Task {
val task = {
get[Long]("id") ~
get[String]("label") ~
get[String]("body") map {
case id ~ label ~ body => Task(id, label, body)
}
}
def all(): List[Task] = DB.withConnection { implicit c =>
SQL("select * from task").as(task *)
}
}
Viewを書く
Viewファイルは以下のようになっています。
├── app
│ └── views
│ ├── index.scala.html
│ └── main.scala.html
main.scala.html
の中身を見ると分かりますが、こちらはrailsでいうlayout/application.html.erb
に、中身の@content
が<%= yield %>
に対応してます。
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<link rel='stylesheet' href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))'>
<script src="@routes.Assets.versioned("javascripts/hello.js")" type="text/javascript"></script>
</head>
<body>
<div class="container">
@content
</div>
</body>
</html>
次にindex.scala.html
の中身を見てみます。htmlとscalaのコードを混ぜて書いても大丈夫なのが凄いですね。
最初の行で、controller
から受け取る引数を定義しています。
@inputText(taskForm("label"))
となっていますが、ここでcontrollerで定義したtaskForm
が関係してきます。taskFormのところで、"label" -> nonEmptyText
のように定義していますが、これによりこのフォームでは空文字が登録できないようvalidationがかかります。
@(tasks: List[Task], taskForm: Form[Task])(implicit messages: Messages)
@import helper._
@main("ToDo List") {
<h1>@tasks.size tasks(s)</h1>
<ul>
@tasks.map { task =>
<li>
@task.label
@task.body
@form(routes.Application.deleteTask(task.id)){
<input type="submit" value="delete" class="btn btn-danger">
}
</li>
}
</ul>
<h2>Add a new task</h2>
@form(routes.Application.newTask) {
@inputText(taskForm("label"))
@inputText(taskForm("body"))
<input type="submit" value="Create" class="btn btn-primary">
}
}
まとめ(感想)
テストまではカバーできませんでしたが、何となくPlayの雰囲気だけでも伝わっていれば幸いです。
冒頭でも書きましたが、Play、Scalaは勉強を始めたばかりなので変なところ・間違っているところがあればご指摘頂けると嬉しいです!