Play Frameworkを使ったWebアプリケーション作成 掲示板作成
ドワンゴ社 新卒エンジニア向けのScala研修資料をやっていくもの ※テスト項目の手前の段階でページが正常に表示される事を確認する
資料
投稿されたメッセージのクラスを作る
package controllers import java.time.OffsetDateTime case class Post(body: String, date: OffsetDateTime)
ケースクラスとは
いつ使うのか
もし、イミュータブルなオブジェクトを使って純粋に関数的なコードを書こうとするならば、通常のクラスは使わないようにした方がよいでしょう。関数型パラダイムの主な考え方は、データ構造とそれに対する操作を分離することです。ケースクラスは、必要なメソッドを持つデータ構造の表現です。データに対する関数は、別のソフトウェアエンティティ (例: traits、object) で記述されるべきです。 これに対し、通常のクラスは、データと操作を結びつけて、ミュータビリティを提供する。この方法は、オブジェクト指向のパラダイムに近い。
な、なるほど〜! 今回でいうと画面からのリクエストメッセージはイミュータブルなデータとして扱うから、caseクラスを使用していると理解した。
投稿されたメッセージの保存と取得
package controllers object PostRepository { var posts: Seq[Post] = Vector() def findAll: Seq[Post] = posts def add(post: Post): Unit = { posts = posts :+ post } }
objectで定義されている理由
自分の理解だが、関数のみを定義しフィールドを保持しないため
クラス名のsufixがRepositoryの理由
- Repositoryパターンにおける、MVC + Service + Repositoryの役割をもう一回整理してみる
- データベースや外部APIなどアプリケーション外部とのやりとりを記載するもの
var posts: Seq[Post] = Vector
Vector
- コレクションクラスの一つで、色んな操作において速度が安定しており、まず最初に検討すると良いクラスの一つだそうな
imutable
なので一度データ構造を構築したら変更出来ない- と言いつつ
Vector(1, 2, 3, 4, 5).updated(2, 5)
みたいな例が書いてあり、更新出来そうに見える? - 自分の頭の中の可変・不変の定義が誤っていた...、変えてそうに見えて別コレクションを返しているそう
- 可変コレクションおよび不変コレクション | Collections | Scala Documentation
Scala のコレクションは、体系的に可変および不変コレクションを区別している。可変 (mutable) コレクションは上書きしたり拡張することができる。これは副作用としてコレクションの要素を変更、追加、または削除することができることを意味する。一方、不変 (immutable) コレクションは変わることが無い。追加、削除、または更新を模倣した演算は提供されるが、全ての場合において演算は新しいコレクションを返し、古いコレクションは変わることがない。
- 可変コレクションおよび不変コレクション | Collections | Scala Documentation
- と言いつつ
Seq
def findAll: Seq[Post] = posts
- 通常であればここはDBから取得した値を返すが、今回は変数を返す仕様となっている
def add(post: Post): Unit = { posts = posts :+ post }
- 変数に対して処理を行なっている理由は上の記載と同様
Unit型とは
- クラス | Scala Documentation
- 一旦はJavaで言うような
void
型であると理解した
- 一旦はJavaで言うような
コントローラ
※私の環境はplay 2.8を使用したため、資料の表記と異なる
package controllers import java.time.OffsetDateTime import javax.inject.Inject import play.api.data.Form import play.api.data.Forms._ import play.api.i18n.I18nSupport import play.api.mvc.{AbstractController, ControllerComponents} case class PostRequest(body: String) class TextboardController @Inject()(cc: ControllerComponents) extends AbstractController(cc) with I18nSupport { private[this] val form = Form( mapping( "post" -> text(minLength = 1, maxLength = 10).withPrefix("hogeika") )(PostRequest.apply)(PostRequest.unapply)) def get = Action { implicit request => Ok(views.html.index(PostRepository.findAll, form)) } def post = Action { implicit request => form.bindFromRequest().fold( error => BadRequest(views.html.index(PostRepository.findAll, error)), postRequest => { val post = Post(postRequest.body, OffsetDateTime.now) PostRepository.add(post) Redirect("/") } ) } }
PlayのForm
private[this] val form = Form( mapping( "post" -> text(minLength = 1, maxLength = 10) )(PostRequest.apply)(PostRequest.unapply))
- バリデーションに関しての内容は以下の説明で理解出来た
(PostRequest.apply)(PostRequest.unapply))
– unapply
- apply
は引数を受け取りオブジェクトを返す、unapply
はオブジェクトから引数を返す
- つまりapplyで値を適用し、unapplyで値を取り出す、一度applyしているため型安全が保証されると理解した
GETのときの動作
- 投稿されたメッセージを全て読み出して、htmlテンプレートを呼び出す
POSTのときの動作
bindFromRequest
を使ってリクエストからパラメータを読み取るfold
メソッドを使って結果を処理- 第一引数がバリデーション失敗時に呼び出す関数
- 第二引数がバリデーション成功時に呼び出す関数
@Inject()とは
- ScalaでDI(Play framework2.7 + Guice編) - Lambdaカクテル
- この記述をしておく事で、DIコンテナから依存するコンポーネントを取得してくる
HTMLテンプレート
@import controllers.Post @import java.time.format.DateTimeFormatter @(posts: Seq[Post], form: Form[PostRequest])(implicit messages: Messages) <!DOCTYPE html> <html lang="ja"> <head> <title>Scala Text Textboard</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="/assets/textboard.css"> </head> <body> <div class="container"> <h1>@Messages("textboard.title")</h1> @for(error <- form.errors){ <p class="text-danger" id="error">@Messages(error.message)</p> } <form method="POST" action="/" class="form-inline"> <input type="text" class="form-control" id="post" name="post"> <button type="submit" class="btn btn-default">@Messages("textboard.send")</button> </form> <table class="table"> <thead> <tr><th>@Messages("textboard.dateTime")</th><th>@Messages("textboard.message")</th></tr> </thead> <tbody> @for(post <- posts.reverse){ <tr> <td class="post-date">@{ val formatter = DateTimeFormatter.ofPattern(Messages("textboard.dateFormat"), messages.lang.toLocale) post.date.format(formatter) }</td> <td class="post-body">@post.body</td> </tr> } </tbody> </table> </div> </body> </html>
Twirlは @Messages のようにHTMLの中で @ が付いている場所をScalaのコードだと解釈します。
テンプレートの引数
index.scala.html というファイルは views.html.index という関数に変換されます。 - そんで例えばgetメソッド等のロジック内で
Ok(views.html.index(PostRepository.findAll, form))
みたく呼び出される - テンプレートが関数に変換されるあたり関数型の思想なのかなと思った
投稿されたメッセージの表示
@for(post <- posts.reverse){ <tr> <td class="post-date">@{ val formatter = DateTimeFormatter.ofPattern(Messages("textboard.dateFormat"), messages.lang.toLocale) post.date.format(formatter) }</td> <td class="post-body">@post.body</td> </tr> }
@{ } は複数行のScalaのコードを書く場合に便利な記法
その他調べたこと
シールド修飾子とは
sealed
修飾子がつけられたクラス- 同一ファイル内に定義されたクラスからしか継承出来ない
- サブクラスがファイル内にしか存在しない事が保証される
コンパニオンオブジェクトの必要性が分からない
コンパニオンオブジェクトって何のためにいるの? for Rubyist - Qiita
applyって何だっけ?
Object構築時の呼ばれる
-
Point(x)のような記述があった場合で、Point objectにapplyという名前のメソッドが定義されていた場合、Point.apply(x)と解釈されます。これを利用してPoint objectの applyメソッドでオブジェクトを生成するようにすることで、Point(3, 5)のような記述でオブジェクトを生成できるようになります。