Play Frameworkハンズオン

  • 30
    いいね
  • 0
    コメント

今日作成するアプリ

画面表示

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と似た同種のフレームワーク
  • ホットリローディング
  • Java版ではサーブレットやJSPを使わない

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

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

3.sbtシェル

1)sbtコマンド

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

$ sbt

2)実行コマンド

> run

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

$ sbt run

4.開発環境チェック

1)Java

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

$ java -version

次のように表示されたら成功です。バージョンは1.8以上であれば大丈夫です。 (2017年7月13日時点の最新バージョン:1.8.0_131)

java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

2)sbt

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

$ sbt sbtVersion

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

[info] Loading global plugins from /Users/yuichi0301/.sbt/0.13/plugins
[info] Loading project definition from /Users/yuichi0301/project
[info] Updating {file:/Users/yuichi0301/project/}yuichi0301-build...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Set current project to yuichi0301 (in build file:/Users/yuichi0301/)
[info] 0.13.15

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 ...]の行を残して他の行を削除します。

次のようになります。

# Map static resources from the /public folder to the /assets URL path
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 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))
  }

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"

9-a-0.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でサーバーを停止したのち再度サーバーを起動します。

sbt 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">
}