PlayFramework

Play Frameworkハンズオン

環境構築 が完了していることを前提とします。


今日作成するアプリ

画面表示

11-0-1.png

登録後

11-0-2.png


1.ScalaとPlay Frameworkの簡単な説明


Scalaについて


  • オブジェクト指向言語と関数型言語の特徴を統合した言語

  • Javaプラットフォーム(Java仮想マシン)上で動作する

  • 既存のJavaのプログラムと容易に連携できるため、Javaの豊富なライブラリが使える


Scalaの採用事例

主な採用事例


  • Twitter

  • 株式会社ドワンゴ

  • 株式会社はてな(mackerel)

  • SmartNews

  • LINE株式会社

  • ヌーラボ(Typetalk, Backlog)

Scalaの採用事例一覧


Play Frameworkについて


  • ScalaとJava言語で書かれたオープンソースのWebアプリケーションフレームワーク

  • Ruby on RailsやDjangoと似た同種のフレームワーク


2.ディレクトリ構成についての説明

.

├── app アプリケーションのソースコード
│   ├── controllers アプリケーションのコントローラ
│   └── views テンプレート
├── build.gradle
├── build.sbt アプリケーションのビルドスクリプト
├── conf アプリケーションの設定ファイル
│   ├── application.conf メイン設定ファイル
│   ├── logback.xml ログ出力設定ファイル
│   ├── messages 国際化対応用言語ファイル
│   └── routes ルート定義
├── gradle
├── gradlew
├── gradlew.bat
├── project sbt 設定ファイル群
│   ├── build.properties sbt プロジェクトの目印
│   ├── plugins.sbt Play 自身の定義を含む sbt プラグイン
│   ├── project
│   ├── scaffold.sbt scaffolding 用の sbt プラグイン使用時に使用
│   └── target
├── public 公開アセット
├── target ビルド成果物
└── test 単体、および機能テスト用のソースフォルダ


3.sbtシェル


1)sbtコマンド

作成したプロジェクト直下でsbtコマンドを実行するとsbtシェルに入り、sbtなしで様々なコマンドを実行することができます。

$ sbt


2)実行コマンド

> run

scalaシェルに入っていない場合は次のコマンドになります

$ sbt run


3) ビルド定義の再読み込み

> reload


ビルド定義(build.sbt、 project/.scala、 project/.sbt ファイル)を再読み込みする。 ビルド定義を変更した場合に必要。



4.開発環境チェック


1)Java

次のコマンドを実行します。

$ java -version

次のように表示されたら成功です。バージョンは1.8以上であれば大丈夫です。

(2019年1月9日時点の最新バージョン:1.8.0_192)

openjdk version "1.8.0_192"

OpenJDK Runtime Environment (build 1.8.0_192-amazon-corretto-preview2-b12)
OpenJDK 64-Bit Server VM (build 25.192-b12, mixed mode)


2)sbt

次のコマンドを実行します。

$ sbt sbtVersion

次のように表示されれば大丈夫です。(バージョンは1.0.0以上である必要があります。)

[info] Loading settings for project play-hands-on-build from plugins.sbt,scaffold.sbt ...

[info] Loading project definition from /Users/yuichi/Desktop/play-hands-on/project
[info] Loading settings for project root from build.sbt ...
[info] Set current project to play-hands-on (in build file:/Users/yuichi/Desktop/play-hands-on/)
[info] 1.2.8
[INFO] [01/09/2019 14:15:34.369] [Thread-3] [CoordinatedShutdown(akka://sbt-web)] Starting coordinated shutdown from JVM shutdown hook


3)プロジェクト

IntelliJを起動し次のように画面が表示されれば大丈夫です。

prepare.png


5.作成前の準備


1)サンプルファイルの削除

この時点でサンプルのファイルが入っているため削除します。

プロジェクトのルートディレクトリ配下のappディレクトリの下にあるファイルを全て削除します。

次のように選択し

5-1.png

右クリックし[Delete]を選択します。(safe deleteのチェックは外します。)

5-2.png

5-3.png


2)routesの不要ルートの削除

5-4.png

play-hands-on/conf/routesファイルで[GET /assets/*file ...]の行を残して他の行を削除します。

次のようになります。

GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)


6.Hello World

目的:コントローラーの書き方、routesの書き方

6-0-1.png

次のような画面を作成します。

6-2-1.png


1)routesにhelloworld用のルートを追加

/play-hands-on/conf/routes


この一行を書いてみましょう


GET     /todo/helloworld                       controllers.TodoController.helloworld()


2)TodoControllerの作成

controllersパッケージにTodoControllerクラスを作成します。

IntelliJ上でcontrollersディレクトリを右クリックし、[New]の[Scala Class]をクリックしします。

6-1.png

表示されたダイアログに[TodoController]と入力しOKをクリックします。

6-2.png

/play-hands-on/app/controllers/TodoController.scala


importの部分はコピーしてそれ以外は書いてください


package controllers

//コピペ
import javax.inject._
import play.api.mvc._

import play.api.data._
import play.api.data.Forms._
//コピペ

//入力
class TodoController @Inject()(mcc: MessagesControllerComponents)
extends MessagesAbstractController(mcc) {

def helloworld() = Action { implicit request: MessagesRequest[AnyContent] =>
Ok("Hello World")
}

}
//入力

メソッド
ステータスコード

Ok
200

BadRequest
400

Forbidden
403

NotFound
404

Redirect
リダイレクト処理


3)サーバー起動

プロジェクト直下で次のコマンドを実行しsbtシェルに入ります。

$ sbt

runコマンドでサーバーを起動します。

> run

次のように表示されます。

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Enter to stop and go back to the console...)


4)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

Hello World画面



次の画面が表示されたら成功です!

6-2-1.png


7.リスト画面の作成(テンプレート)

目的:テンプレートの使い方、Scalaの変数の書き方

7-a-1.png

次のような画面を作成します。

7-0.png


1)routesにlist用のルートを追加

/play-hands-on/conf/routes


この一行を書いてみましょう


GET     /todo                       controllers.TodoController.list()


2)TodoControllerにlistメソッドを追加

次のlistメソッドを追加します。

/play-hands-on/app/controllers/TodoController.scala


こちらのメソッドを実装してください


  def list() = Action { implicit request: MessagesRequest[AnyContent] =>

val message: String = "ここにリストを表示"
Ok(views.html.list(message))
}


varはmutableな変数を定義する時に使用します。再代入可であることを示します。

valはimmutableな変数を定義する時に使用します。再代入不可であることを示します。



3)listテンプレートの作成

/play-hands-on/app/viewsの下に[list]テンプレートを作成します。

IntelliJ上でviewsディレクトリを右クリックし、[New]の[File]をクリックします。

7-1.png

表示されたダイアログに[list.scala.html]と入力しOKをクリックします。

7-2.png


こちらのHTMLをコピーして貼り付けてください


ここを実装してください

<html>
<head>
<title>Todo</title>
</head>
<body>

<section>
ここを実装してください
</section>

</body>
</html>

/play-hands-on/app/views/list.scala.html


[ここを実装してください]と書いている箇所に次の内容と同じものを実装してください


@(message: String)

<html>
<head>
<title>Todo</title>
</head>
<body>

<section>
@message
</section>

</body>
</html>


4)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

リスト画面



次の画面が表示されたら成功です!

7-0.png


8.リスト画面の作成(case class)

目的:case class、Seq()、viewでのループ

8-0.png


1)サービスの作成


servicesパッケージを作成する

appディレクトリを右クリックし、[New]の[Package]をクリックします。

8-1-1.png

表示されたダイアログに[services]と入力しOKをクリックします。

8-2.png


Todoクラスを作成する

servicesパッケージの下にTodoクラスを作成します。

IntelliJ上でservicesディレクトリを右クリックし、[New]の[Scala Class]をクリックします。

8-3-1.png

表示されたダイアログに[Todo]と入力しOKをクリックします。

8-4.png

/play-hands-on/app/services/Todo.scala


こちらの内容を実装してください


package services

case class Todo(name: String)


case classのいいところ

- プロパティが公開される(todo.nameなど)

- インスタンスを作る時にnewが必要なくなる(val todo = Todo())

- equals,hashCode,toStringなどが実装される



2)TodoControllerの編集

/play-hands-on/app/controllers/TodoController.scala


servicesのパッケージを追加します


import services._


listメソッドを編集します


  def list() = Action { implicit request: MessagesRequest[AnyContent] =>

val items: Seq[Todo] = Seq(Todo("Todo1"), Todo("Todo2"))
Ok(views.html.list(items))
}


3)listテンプレートの編集

/play-hands-on/app/views/list.scala.html


こちらのHTMLをコピーして上書きして貼り付けてください


ここを実装してください

<html>
<head>
<title>Todo</title>
</head>
<body>

<section>
<table>
<thead>
<tr>
<th>名前</th>
</tr>
</thead>
<tbody>
ここを実装してください
</tbody>
</table>
<a href="/todo/new">登録画面</a>
</section>

</body>
</html>

/play-hands-on/app/views/list.scala.html


[ここを実装してください]と書いている箇所に次の内容と同じものを実装してください


@import services._

@(items: Seq[Todo])

<html>
<head>
<title>Todo</title>
</head>
<body>

<section>
<table>
<thead>
<tr>
<th>名前</th>
</tr>
</thead>
<tbody>
@items.map { todo =>
<tr>
<td>@todo.name</td>
</tr>
}
</tbody>
</table>
<a href="/todo/new">登録画面</a>
</section>

</body>
</html>


リバースルーティングを使用したリンクの書き方 href="@controllers.routes.TodoController.todoNew()"



4)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

リスト画面



次の画面が表示されたら成功です!

8-0.png


9.データベースの設定

目的:evolutions、anormの使い方


1)evolutionsについて

今回データベースを使用するにあたりDBマイグレーションツールを使用します。DBマイグレーションツールとは、スキーマのバージョン管理のようなもので、スキーマの変更に対してデータベースを追随させることができます。evolutionsはPlayframeworkで使われているDBマイグレーションツールです。


2)Anormについて

データベースアクセスにはAnormというライブラリを使用します。SQLを直接記述できSQL実行/結果解析をサポートします。


3)build.sbtの設定

プロジェクトルートディレクトリ配下のbuild.sbtを編集します。

9-1.png

次の4行を追加してください。

/play-hands-on/build.sbt


コピーして貼り付けてください


libraryDependencies += jdbc

libraryDependencies += evolutions
libraryDependencies += "com.typesafe.play" %% "anorm" % "2.5.3"
libraryDependencies += "com.h2database" % "h2" % "1.4.196"

「Import Changes」をクリックするとライブラリの情報をIntelliJに読み込んで、ライブラリ内のクラスを自由に参照できるようになります。

pasted-2019.01.11-15.15.06.png


4)evolutionsの設定ファイルの作成

/play-hands-on/confディレクトリの下に[evolutions/default]ディレクトリを作成します。

9-2.png

IntelliJ上で[conf]ディレクトリを右クリックし、[New]の[Directory]をクリックします。

9-3.png

表示されたダイアログに[evolutions/default]と入力しOKをクリックします。

9-4.png


5)マイグレーションファイルの作成

作成したディレクトリにマイグレーションファイルを作成します。

IntelliJ上で[evolutions.default]ディレクトリを右クリックし、[New]の[File]をクリックします。

9-5.png

表示されたダイアログに[1.sql]と入力しOKをクリックします。

9-6.png

/play-hands-on/conf/evolutions/default/1.sql


コピーして貼り付けてください


# --- First database schema

# --- !Ups
create table todo (
id bigint not null auto_increment,
name varchar(255) not null,
constraint pk_todo primary key (id))
;
create sequence todo_seq start with 1000;

insert into todo (id,name) values (1,'書類の整理');
insert into todo (id,name) values (2,'本の返却');

# --- !Downs
drop table if exists todo;

drop sequence if exists todo_seq;


6)データベースの設定

confディレクトリの下のapplication.confに次の2行を追加します。

9-7.png

/play-hands-on/conf/application.conf


コピーして貼り付けてください


db.default.driver = org.h2.Driver

db.default.url = "jdbc:h2:mem:play"

H2 Databaseと言うJavaプラットフォーム上で動く、インメモリデータベースを使用するように設定されます。


7)データベースを作成しましょう

一旦、Ctrl + Dでサーバーを停止したのち、ビルド定義を再読み込みしサーバーを起動します。

> reload

> run

次のリンクをクリックします。

リスト表示画面



データベース作成画面が表示されます。

[Apply this script now!]ボタンをクリックしデータベースを作成しましょう。

9-0.png


10.リスト画面の作成(データベース)

目的:Scalaでのデータベースの使い方

10-a-2.png


10-0.png


1)モデルの編集

/play-hands-on/app/services/Todo.scala


全部コピーして上書きしてください


package services

import javax.inject.Inject

import anorm.SqlParser._
import anorm._
import play.api.db.DBApi

import scala.language.postfixOps

case class Todo(name: String)

@javax.inject.Singleton
class TodoService @Inject() (dbapi: DBApi) {

private val db = dbapi.database("default")

val simple = {
get[String]("todo.name") map {
case name => Todo(name)
}
}

def list(): Seq[Todo] = {

db.withConnection { implicit connection =>

SQL(
"""
select * from todo
"""

).as(simple *)

}

}

}


2)TodoControllerでTodoServiceを使えるように設定する

/play-hands-on/app/controllers/TodoController.scala


todoService: TodoService,を追加してください


class TodoController @Inject()(todoService: TodoService, mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) {


3)TodoControllerのlistメソッドの編集

/play-hands-on/app/controllers/TodoController.scala


こちらの内容で編集してください


  def list() = Action { implicit request: MessagesRequest[AnyContent] =>

val items: Seq[Todo] = todoService.list()
Ok(views.html.list(items))
}


4)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

リスト画面



次の画面が表示されたら成功です!

10-0.png


11.登録画面の作成(テンプレート)

目的:Playのフォームテンプレートヘルパーの使い方

11-a-1.png

画面表示

10-0-1.png

登録後

10-0-2.png


1)routesに登録画面表示と登録用のルートを追加

/play-hands-on/conf/routes


次の2行を追加してください


GET     /todo/new                   controllers.TodoController.todoNew()

POST /todo controllers.TodoController.todoAdd()


2)TodoControllerにメソッドを追加

/play-hands-on/app/controllers/TodoController.scala


次の2つのメソッドを実装してください


  val todoForm: Form[String] = Form("name" -> nonEmptyText)

def todoNew = Action { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.createForm(todoForm))
}

def todoAdd() = Action { implicit request: MessagesRequest[AnyContent] =>
val name: String = todoForm.bindFromRequest().get
println(name)
Ok("Save")
}


3)createFormテンプレートの作成

/play-hands-on/app/viewsの下にcreateFormテンプレートを作成します。

IntelliJ上で[views]パッケージを右クリックし、[New]の[File]をクリックします。

11-1.png

表示されたダイアログに[createForm.scala.html]と入力しOKをクリックします。

11-2.png

/play-hands-on/app/views/createForm.scala.html


こちらのHTMLをコピーして貼り付けてください


ここを実装してください

<html>
<head>
<title>Todo</title>
</head>
<body>

<h1>Todo登録</h1>

ここを実装してください

</body>
</html>

/play-hands-on/app/views/createForm.scala.html


[ここを実装してください]と書いている箇所に次の内容と同じものを実装してください


@(todoForm: Form[String])(implicit request: MessagesRequestHeader)

<html>
<head>
<title>Todo</title>
</head>
<body>

<h1>Todo登録</h1>

@helper.form(action = routes.TodoController.todoAdd()) {
@helper.CSRF.formField

<fieldset>

@helper.inputText(todoForm("name"), '_label -> "名前")

</fieldset>

<input type="submit" value="登録">

}

</body>
</html>


4)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

登録画面



次の画面が表示されたら成功です!

画面表示

10-0-1.png

登録後

10-0-2.png


12.登録画面の作成(データベース)

目的:データベースの利用(登録編)

12-b-1.png

画面表示

11-0-1.png

登録後

11-0-2.png


1)TodoServiceにメソッドを追加します。

/play-hands-on/app/services/Todo.scala

TodoServiceの中


コピーして追加してください


  def insert(todo: Todo) = {

db.withConnection { implicit connection =>
SQL(
"""
insert into todo values ((select next value for todo_seq), {name})
"""

).on(
'name -> todo.name
).executeUpdate()
}
}


2)TodoControllerのtodoAddメソッドを次のように編集します。

/play-hands-on/app/controllers/TodoController.scala


次の内容で編集してください


  def todoAdd() = Action { implicit request: MessagesRequest[AnyContent] =>

val name: String = todoForm.bindFromRequest().get
todoService.insert(Todo(name))
Redirect(routes.TodoController.list())
}


3)画面をブラウザで見てみましょう

こちらのリンクをクリックしてください

登録画面



次の画面が表示されたら成功です!

画面表示

11-0-1.png

登録後

11-0-2.png


13.更新処理の実装


1)routesにupdate用のルートを追加

/play-hands-on/conf/routes


次の2行を追加してください


GET     /todo/edit/:todoId                  controllers.TodoController.todoEdit(todoId:Long)

POST /todo/:todoId controllers.TodoController.todoUpdate(todoId:Long)


2)TodoControllerにメソッドを追加

/play-hands-on/app/controllers/TodoController.scala


次の2つのメソッドを実装してください


  def todoEdit(todoId: Long) = Action { implicit request: MessagesRequest[AnyContent] =>

todoService.findById(todoId).map { todo =>
Ok(views.html.editForm(todoId, todoForm.fill(todo.name)))
}.getOrElse(NotFound)
}

def todoUpdate(todoId: Long) = Action { implicit request: MessagesRequest[AnyContent] =>
val name: String = todoForm.bindFromRequest().get
todoService.update(todoId, Todo(Some(todoId), name))
Redirect(routes.TodoController.list())
}

todoAddメソッドを次のように編集します。Todoにidが増えたのでNoneを渡しています。

  def todoAdd() = Action { implicit request: MessagesRequest[AnyContent] =>

val name: String = todoForm.bindFromRequest().get
todoService.insert(Todo(id = None, name))
Redirect(routes.TodoController.list())
}


3)TodoServiceにメソッドを追加し、Todoにidを追加します。

/play-hands-on/app/services/Todo.scala

package services

import javax.inject.Inject

import anorm.SqlParser._
import anorm._
import play.api.db.DBApi

import scala.language.postfixOps

case class Todo(id:Option[Long], name: String)

@javax.inject.Singleton
class TodoService @Inject() (dbapi: DBApi) {

private val db = dbapi.database("default")

val simple = {
get[Option[Long]]("todo.id") ~
get[String]("todo.name") map {
case id~name => Todo(id, name)
}
}

def list(): Seq[Todo] = {

db.withConnection { implicit connection =>

SQL(
"""
select * from todo
"""

).as(simple *)

}

}

def insert(todo: Todo) = {
db.withConnection { implicit connection =>
SQL(
"""
insert into todo values ((select next value for todo_seq), {name})
"""

).on(
'name -> todo.name
).executeUpdate()
}
}

def findById(id: Long): Option[Todo] = {
db.withConnection { implicit connection =>
SQL("select * from todo where id = {id}").on('id -> id).as(simple.singleOpt)
}
}

def update(id: Long, todo: Todo) = {
db.withConnection { implicit connection =>
SQL(
"""
update todo
set name = {name}
where id = {id}
"""

).on(
'id -> id,
'name -> todo.name
).executeUpdate()
}
}

}


4)editFormテンプレートの作成

/play-hands-on/app/views/editForm.scala.html

@(id: Long, todoForm: Form[String])(implicit request: MessagesRequestHeader)

<html>
<head>
<title>Todo</title>
</head>
<body>

<h1>Todo更新</h1>

@helper.form(action = routes.TodoController.todoUpdate(id)) {
@helper.CSRF.formField

<fieldset>

@helper.inputText(todoForm("name"), '_label -> "名前")

</fieldset>

<input type="submit" value="更新">

}

</body>
</html>


5)list.scala.htmlのリストをリンクにします

@import services._

@(items: Seq[Todo])

<html>
<head>
<title>Todo</title>
</head>
<body>

<section>
<table>
<thead>
<tr>
<th>名前</th>
</tr>
</thead>
<tbody>
@items.map { todo =>
<tr>
<td><a href="@controllers.routes.TodoController.todoEdit(todo.id.get)">@todo.name</a></td>
</tr>
}
</tbody>
</table>
<a href="@controllers.routes.TodoController.todoNew()">登録画面</a>
</section>

</body>
</html>


14.削除処理の実装


1)routesにdelete用のルートを追加

/play-hands-on/conf/routes


次の行を追加してください


POST   /todo/:todoId/delete                controllers.TodoController.todoDelete(todoId:Long)


2)TodoServiceに削除メソッドを追加します

/play-hands-on/app/services/Todo.scala

  def delete(id: Long) = {

db.withConnection { implicit connection =>
SQL("delete from todo where id = {id}").on('id -> id).executeUpdate()
}
}


3)TodoControllerにメソッドを追加

/play-hands-on/app/controllers/TodoController.scala


次のメソッドを実装してください


  def todoDelete(todoId: Long) = Action { implicit request: MessagesRequest[AnyContent] =>

todoService.delete(todoId)
Redirect(routes.TodoController.list())
}


4)editFormテンプレートの編集

削除ボタンを追加します。

/play-hands-on/app/views/editForm.scala.html

@helper.form(action = routes.TodoController.todoDelete(id)) {

@helper.CSRF.formField
<input type="submit" value="削除" class="btn danger">
}