Scala & Play2 学習ノート(2)
Scala 学習ノート二回目。やっぱWebアプリを作らなきゃということで PlayFramework を使っていきます。
Play2 を使う
bizreach のハンズオンが良さげなのでとりあえず流しでやってみます。
プロジェクトの作成
- プロジェクトを作る
既にsbt、IntelliJは入っているので新規プロジェクトを作る所から。
bash-3.2$ sbt new playframework/play-scala-seed.g8 --branch 2.6.x
[info] Set current project to ideaprojects (in build file:/Users/yossy6954/IdeaProjects/)
[info] Set current project to ideaprojects (in build file:/Users/yossy6954/IdeaProjects/)
This template generates a Play Scala project
name [play-scala-seed]: play2-hands-on
organization [com.example]:
play_version [2.6.15]:
sbt_version [1.1.2]:
scalatestplusplay_version [3.1.2]:
Template applied in ./play2-hands-on
build.sbt を書き換え、sbt runで http://localhost:9000 で起動する事を確認
IDEの準備
特に問題なし
DBの準備
- h2 Databaseの起動
- plugin.sbt に scalikejdbc-mapper-generator を追加
- project/scalikejdbc.properties を追加
- ScalikejdbcPlugin の有効化
上記設定後、sbt "scalikejdbcGenAll" でDBスキーマからモデルクラスを生成。いわゆるDBファーストってやつですね。
モデル作成後、application.conf にDB接続を追加。
sbt でプラグインを起動する時のDB設定とPlay2アプリのDB設定は別という事でしょうか。
ルーティングの定義
-
BootstrapのCSS/JavaScriptの追加
-
CSPの無効化
ASP.NET Coreと違ってCSPはデフォルトで有効なんですね
-
UserController の作成
package controllers
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import javax.inject.Inject
import scalikejdbc._
import models._
class UserController @Inject()(components: MessagesControllerComponents)
extends MessagesAbstractController(components) {
/**
* 一覧表示
*/
def list = TODO
/**
* 編集画面表示
*/
def edit(id: Option[Long]) = TODO
/**
* 登録実行
*/
def create = TODO
/**
* 更新実行
*/
def update = TODO
/**
* 削除実行
*/
def remove(id: Long) = TODO
}
@Inject はDIの為のアノテーション。この場合は MessagesControllerComponents がDIされる事を示す。
また、TODOは ControllerHelper で定義されていて呼び出すとTODOページを返す。
lazy val TODO: Action[AnyContent] = ActionBuilder.ignoringBody {
NotImplemented[Html](views.html.defaultpages.todo())
}
-
ルーティングの定義
conf/routes に"メソッド パス コントローラ(パラメータ)" みたいな感じで書く。
ユーザ一覧の実装
- list.scala.html の実装
- UserController.list の実装
def list = Action { implicit request =>
val u = Users.syntax("u")
DB.readOnly { implicit session =>
// ユーザのリストを取得
val users = withSQL {
select.from(Users as u).orderBy(u.id.asc)
}.map(Users(u.resultName)).list.apply()
// 一覧画面を表示
Ok(views.html.user.list(users))
}
}
DBに読み込み専用のセッションを貼り、Userをid昇順で取得してUsersのリストに詰め、user.list のテンプレートの引数に渡して出力。
テンプレートは @* 〜 *@ がコメント、 @〜 だと Scalaの構文が実行される感じでしょうか。
テンプレートを読んで何が起こるかは想像がつきますが一から書けと言われるとちょっと勉強が必要そうです。
WebAPI全盛の今ドキュメントベースのWebアプリを作る機会はそんなに無いので実際に使うことがあるかどうかわかりませんしね。思えばASP.NETのRazorもほとんど使わなかったし。
ユーザ登録・編集画面の実装
-
UserController コンパニオンオブジェクトの実装
コンパニオンオブジェクト内にフォームの値を格納するケースクラス及び変換メソッドの定義
-
edit.scala.html (ユーザ編集用ビュー)の実装
-
UserController.edit の実装
idの指定があった時はidのユーザ情報をDBから取得し、Formに詰めて edit.scala.htmlを表示。
無かった時は空Formで同上。
テンプレート側でformのPOST先をidの有無によってcreate/updateで呼び分けている。
登録・更新処理の実装
-
UserController.create, UserController.update の実装
ほぼ同じ実装。
DB.localTx{} でトランザクション処理ができる。commit() みたいなものの呼び出しは不要。
form.bindFromRequest.fold でリクエストのPOST内容を UserForm ケースクラスに展開できる。この際バリデーションも行われる。
削除処理の実装
- UserController.remove の実装
idで検索したユーザを destroy() するだけ。
ジョインの必要な処理
- UserController.list の修正
- list.scala.html (ユーザ一覧ビュー)の修正
def list = Action { implicit request =>
/* ↓追加 */
val u = Users.syntax("u")
val c = Companies.syntax("c")
/* ↑追加 */
DB.readOnly { implicit session =>
// ユーザのリストを取得
val users = withSQL {
select.from(Users as u).leftJoin(Companies as c).on(u.companyId, c.id).orderBy(u.id.asc)
}.map { rs =>
(Users(u)(rs), rs.intOpt(c.resultName.id).map(_ => Companies(c)(rs)))
}.list.apply()
// 一覧画面を表示
Ok(views.html.user.list(users))
}
}
解説ページのソースだと syntax() の呼び出しが欠けている模様。
withSQL で left join を使ったSQLを発行し、map でレコードが company_id を持っていた場合は Companies に詰めて
(Users, Companies)のタプルを作ってビューの引数にしている。
JSON APIの準備
- JsonController の作成
- JSON API用ルーティングの定義
UserController 作成時と同様空のコントローラを作成する。
国際化機能は使わないので ControllerComponents を DIする。
ユーザ一覧APIの実装
-
JsonController コンパニオンオブジェクトの実装
コンパニオンオブジェクト内に Users を JSON に書き出すメソッド usersWrites() を定義
-
JsonController.list の実装
処理は UsersController.list と同じ。最後だけ Json.obj("users" -> users) で JSONオブジェクトを作って返す。
ちなみに JsonContoller コンパニオンオブジェクトの定義を JsonController より後ろに書くと実行時に Cmpilation error が出ます。
type mismatch;
found : List[models.Users]
required: play.api.libs.json.Json.JsValueWrapper
Note: implicit value usersWrites is not applicable here because it comes after the application point and it lacks an explicit result type
暗黙的な型変換で結果型が明記されていない場合は実際の使用位置より前で宣言しておかないとダメだとか。
ユーザ登録・更新APIの実装
- JsonController コンパニオンオブジェクトに usersReads() を実装
- JsonController.create, edit の実装
今度は usersReads() を実装してJSONから UserForm に変換するメソッドを実装。
create/update は Form版とほぼ同じ流れ。
def create = Action(parse.json) { implicit request =>
request.body.validate[UserForm].map { form =>
// OKの場合はユーザを登録
DB.localTx { implicit session =>
Users.create(form.name, form.companyId)
Ok(Json.obj("result" -> "success"))
}
}.recoverTotal { e =>
// NGの場合はバリデーションエラーを返す
BadRequest(Json.obj("result" -> "failure", "error" -> JsError.toJson(e)))
}
}
Action(parse.json) でリクエストボディからJSON受け取り。
request.body.validate[UserForm]でバリデーション・変換。
バリデーション失敗時は .recoverTotal で失敗時の処理を記述する。
ユーザ削除APIの実装
- JsonController.remove の実装
ScalikeJDBCのテスト
- scalikejdbc-testライブラリの追加
- テスト用DB設定追加(test.conf)
- テスト時にテスト用DBを読み込む設定を追加(build.sbt)
これをしたい時にどのファイルをいじればいいのかが分かりにくくて面倒ですね。慣れの問題でしょうが。
- テストコードの修正
テスト用DBのセットアップとテストが通るようにテストケースを修正します。
全部終えると
$ sbt test
...
[info] - should perform batch insert
[info] ScalaTest
[info] Run completed in 5 seconds, 367 milliseconds.
[info] Total number of tests run: 23
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 23, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 23, Failed 0, Errors 0, Passed 23
[success] Total time: 13 s, completed 2018/06/15 19:18:36
[INFO] [06/15/2018 19:18:36.176] [Thread-2] [CoordinatedShutdown(akka://sbt-web)] Starting coordinated shutdown from JVM shutdown hook
Play2のテスト
- JsonControllerSpec の実装
FakeRequest というものを使ってダミーのリクエストを送信し、コントローラのメソッドを叩いて結果を確認します。
今まで私はWebApp作る時は Controller は薄くしてビジネスロジックは別クラスに実装していたパターンが多いので
正直余りControllerのテストに必要性を感じた事はありませんでした。まあ Play2 ではこういうやり方で Controller のテストができるということで。
まとめ
とりあえず駆け足で手だけ動かしてみました。
何度か愚痴ったような気がするけどやはりよく調べずにコピペしてると実際にはどこで何しているかよくわからなくて辛いですね。
あと Scala 自体何してるのかよくわからない書き方が多いのも。まあその辺りも含め順次勉強ということで。
お次は AWS あたりで今回作ったアプリを動かす方法でも調べたいと思います。