※ 環境構築 が完了していることを前提とします。
今日作成するアプリ
画面表示
登録後
1.ScalaとPlay Frameworkの簡単な説明
Scalaについて
- オブジェクト指向言語と関数型言語の特徴を統合した言語
- Javaプラットフォーム(Java仮想マシン)上で動作する
- 既存のJavaのプログラムと容易に連携できるため、Javaの豊富なライブラリが使える
Scalaの採用事例
主な採用事例
- 株式会社ドワンゴ
- 株式会社はてな(mackerel)
- SmartNews
- LINE株式会社
- ヌーラボ(Typetalk, Backlog)
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を起動し次のように画面が表示されれば大丈夫です。
5.作成前の準備
1)サンプルファイルの削除
この時点でサンプルのファイルが入っているため削除します。
プロジェクトのルートディレクトリ配下のappディレクトリの下にあるファイルを全て削除します。
次のように選択し
右クリックし[Delete]を選択します。(safe deleteのチェックは外します。)
2)routesの不要ルートの削除
play-hands-on/conf/routesファイルで[GET /assets/*file ...]の行を残して他の行を削除します。
次のようになります。
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
6.Hello World
目的:コントローラーの書き方、routesの書き方
次のような画面を作成します。
1)routesにhelloworld用のルートを追加
/play-hands-on/conf/routes
この一行を書いてみましょう
GET /todo/helloworld controllers.TodoController.helloworld()
2)TodoControllerの作成
controllersパッケージにTodoControllerクラスを作成します。
IntelliJ上でcontrollersディレクトリを右クリックし、[New]の[Scala Class]をクリックしします。
表示されたダイアログに[TodoController]と入力しOKをクリックします。
/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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です!7.リスト画面の作成(テンプレート)
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]をクリックします。
表示されたダイアログに[list.scala.html]と入力しOKをクリックします。
こちらの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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です!8.リスト画面の作成(case class)
1)サービスの作成
servicesパッケージを作成する
appディレクトリを右クリックし、[New]の[Package]をクリックします。
表示されたダイアログに[services]と入力しOKをクリックします。
Todoクラスを作成する
servicesパッケージの下にTodoクラスを作成します。
IntelliJ上でservicesディレクトリを右クリックし、[New]の[Scala Class]をクリックします。
表示されたダイアログに[Todo]と入力しOKをクリックします。
/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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です!9.データベースの設定
目的:evolutions、anormの使い方
1)evolutionsについて
今回データベースを使用するにあたりDBマイグレーションツールを使用します。DBマイグレーションツールとは、スキーマのバージョン管理のようなもので、スキーマの変更に対してデータベースを追随させることができます。evolutionsはPlayframeworkで使われているDBマイグレーションツールです。
2)Anormについて
データベースアクセスにはAnormというライブラリを使用します。SQLを直接記述できSQL実行/結果解析をサポートします。
3)build.sbtの設定
プロジェクトルートディレクトリ配下のbuild.sbtを編集します。
次の4行を追加してください。
/play-hands-on/build.sbt
コピーして貼り付けてください
libraryDependencies += jdbc
libraryDependencies += evolutions
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.4"
libraryDependencies += "com.h2database" % "h2" % "1.4.196"
「Import Changes」をクリックするとライブラリの情報をIntelliJに読み込んで、ライブラリ内のクラスを自由に参照できるようになります。
4)evolutionsの設定ファイルの作成
/play-hands-on/confディレクトリの下に[evolutions/default]ディレクトリを作成します。
IntelliJ上で[conf]ディレクトリを右クリックし、[New]の[Directory]をクリックします。
表示されたダイアログに[evolutions/default]と入力しOKをクリックします。
5)マイグレーションファイルの作成
作成したディレクトリにマイグレーションファイルを作成します。
IntelliJ上で[evolutions.default]ディレクトリを右クリックし、[New]の[File]をクリックします。
表示されたダイアログに[1.sql]と入力しOKをクリックします。
/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行を追加します。
/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!]ボタンをクリックしデータベースを作成しましょう。10.リスト画面の作成(データベース)
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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です!11.登録画面の作成(テンプレート)
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]をクリックします。
表示されたダイアログに[createForm.scala.html]と入力しOKをクリックします。
/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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です! 画面表示12.登録画面の作成(データベース)
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)画面をブラウザで見てみましょう
こちらのリンクをクリックしてください
次の画面が表示されたら成功です! 画面表示 登録後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">
}