Rails
Scala
PlayFramework

RailsエンジニアのためのPlay Framework入門

More than 3 years have passed since last update.

最近は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のアプリです。

こんな感じ。

貼り付けた画像_2015_12_06_14_07.png


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を準備する時間も勿体無いなので、こちらを使っても良いかもしれません。

貼り付けた画像_2015_12_06_18_15.png


接続するデータベースの情報を書く(=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が反映されます。

Kobito.9vzAPy.png


外部ライブラリを導入する(= Gemfile )

sbtというツールを使って管理するようです。アプリケーションrootに配置されたbuild.sbtにはslickなどアプリケーションで実際に使用するライブラリを、project/plugins.sbtにはcoffee scriptlessのコンパイルを行うためのものを記述するっぽい。

├── build.sbt

├── project
│   ├── build.properties
│   ├── play-fork-run.sbt
│   ├── plugins.sbt
│   ├── project
│   ├── sbt-ui.sbt
│   └── target

サンプルコードでは以下のように、bootstrapbuild.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は勉強を始めたばかりなので変なところ・間違っているところがあればご指摘頂けると嬉しいです!