Scala + Playのチュートリアル

2024/04/25に公開

はじめに

Scalaの勉強のためにPlay Frameworkを触ってみました。せっかくなので、(雑ではありますが、)チュートリアル形式でまとめてみます。
基本構文やFutureEitherを使ってみて、なんとなく理解することを目的としています。

参考にしたページ

環境情報

名称 バージョン
sbt 1.9.8
Java adoptopenjdk-21.0.2+13.0.LTS
Scala 2.13.13
Play 3.0.2

テンプレートの取得

今回はPlay Frameworkのテンプレートとして提供されているplay-scala-seed.g8をもとにサンプルアプリケーションを作成します。

zsh
sbt new playframework/play-scala-seed.g8

上記コマンドを実行するとnameorganizationの入力が求められますが、すべてデフォルト(何も入力せずにEnter)でもOKです。

zsh
❯ sbt new playframework/play-scala-seed.g8
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
This template generates a Play Scala project

name [play-scala-seed]:
organization [com.example]:
play_version [3.0.2]:
scala_version [2.13.13]:
sbt_giter8_scaffold_version [0.16.2]:

Template applied in {YOUR PATH}/scala-play-tutorial/./play-scala-seed

生成されたファイルをいじらずに一旦起動させてみます。

zsh
cd play-scala-seed
sbt run

起動完了後、 http://localhost:9000 にアクセスすると下記の画面が表示されます。

ユーザー参照、登録

まずは下記のステップでユーザー関連の画面を作成してみます。

  1. GET /usersを実行できる
  2. ユーザー一覧を表示できる(GETを実行してDBからデータを取得できる)
  3. ユーザーを登録できる

Step3完了時点の画面イメージはこんな感じです。

Step1: GET /usersを実行できる

1つ目のステップではGET /usersのエンドポイントを新しく作ります。この段階では一旦静的なレスポンスを返却するようにします。

/usersに対応するControllerとして、下記のファイルを新規作成します。

app/controllers/UsersController.scala
package controllers

import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request

@Singleton
class UsersController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

  def index = Action { implicit request =>
    (Ok("OK!!"))
  }
}

routesGET /usersUsersController.index()のマッピングを行います。

conf/routes
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~

# An example controller showing a sample home page
GET     /                           controllers.HomeController.index()
+GET     /users                      controllers.UsersController.index()

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

動作確認

上記変更を行い、localhost:9000/usersにアクセスすると、下記の画面が表示されます。

Step2: ユーザー一覧を表示できる

このステップで行うことは下記です。

  • DBでテーブル作成とデータ投入を行う
  • DBからデータを取得するメソッドを作成する
  • Controllerから↑のメソッドを呼び出す
  • 画面にデータを表示する

今回はデータ操作ライブラリとしてSlick、マイグレーションツールとしてEvolutions、DBとしてH2を利用するので、設定ファイルを変更します。

build.sbt
name := """play-scala-seed"""
organization := "com.example"

version := "1.0-SNAPSHOT"

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.13.13"

-libraryDependencies += guice
-libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.0" % Test
+libraryDependencies ++= Seq(
+    guice,
+    "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.0" % Test,
+    "org.playframework" %% "play-slick" % "6.1.0",
+    "org.playframework" %% "play-slick-evolutions" % "6.1.0",
+    "com.h2database" % "h2" % "2.2.224"
+)

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "com.example.controllers._"

// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "com.example.binders._"
conf/application.conf
# https://www.playframework.com/documentation/latest/Configuration
+slick.dbs.default.profile="slick.jdbc.H2Profile$"
+slick.dbs.default.db.driver="org.h2.Driver"
+slick.dbs.default.db.url="jdbc:h2:mem:play;DB_CLOSE_DELAY=-1"
+slick.dbs.default.db.user=sa
+slick.dbs.default.db.password=""
+
+play.evolutions.db.default.autoApply=true

それぞれのバージョンは下記を参考にして指定しました。
https://github.com/playframework/play-slick?tab=readme-ov-file#all-releases
https://www.h2database.com/html/main.html

次にUSERSテーブル関連のマイグレーションファイルを作成します。
conf/evolutions/{DB名}/{数字}.sqlのファイルにクエリを書くことで、数字の小さいものからマイグレーションが実行されるようになります。
ファイル名の数字は自然数(e.g. 1, 2, ...)しかサポートされていないようです。

conf/evolutions/default/1.sql
# --- !Ups

CREATE TABLE "USERS" (
    "ID" INT AUTO_INCREMENT PRIMARY KEY,
    "NAME" VARCHAR NOT NULL,
    "AGE" INT NOT NULL
);

INSERT INTO "USERS" ("NAME", "AGE") VALUES ('Alice', 30);
INSERT INTO "USERS" ("NAME", "AGE") VALUES ('Bob', 25);
INSERT INTO "USERS" ("NAME", "AGE") VALUES ('Charlie', 35);

# --- !Downs

DROP TABLE "USERS";

次にUserModelを作成します。

app/models/UserModel.scala
package models

final case class User(
    id: Option[Int],
    name: String,
    age: Int
)

次にUsersのDAOを作成します。
UsersTableでテーブル情報を定義して、それを使ってall()でUsersテーブルからデータを取得します。取得したデータはUserModelとして返却します。

app/dao/UsersDao.scala
package dao

import scala.concurrent.Future

import javax.inject.Inject
import play.api.db.slick.DatabaseConfigProvider
import play.api.db.slick.HasDatabaseConfigProvider
import scala.concurrent.ExecutionContext
import slick.jdbc.JdbcProfile
import models.User

class UsersDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
extends HasDatabaseConfigProvider[JdbcProfile] {
  import profile.api._

  private val Users = TableQuery[UsersTable]

  def all()(implicit ec: ExecutionContext): Future[Seq[User]] = db.run(Users.result)

  private class UsersTable(tag: Tag) extends Table[User](tag, "USERS") {
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def age = column[Int]("AGE")

    def * = (id.?, name, age) <> ((User.apply _).tupled, User.unapply)
  }
}

ControllerからDAOを呼ぶようにします。

app/controllers/UsersController.scala
package controllers

+import scala.concurrent.ExecutionContext
import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request
import scala.concurrent.Future
+import dao.UsersDao
+import models.User

@Singleton
-class UsersController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
+class UsersController @Inject()(dao: UsersDao, cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) {

-  def index = Action { implicit request: Request[AnyContent] =>
-    Ok("OK!!")
+  def index = Action.async { implicit request =>
+    dao.all().map {
+      u => Ok(views.html.users(u))
+    }
  }
}

最後にユーザー一覧画面のhtmlを作成します。
Seq[User]を受け取って表として表示するだけのものです。

app/views/users.scala.html
@(users: Seq[User])

@main("Users") {
<div>
  <div id="users">
    <h1>Users</h1>
    <table>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
      </tr>
      @for(u <- users){
        <tr>
          <td>@u.id</td>
          <td>@u.name</td>
          <td>@u.age</td>
        </tr>
      }
    </table>
  </div>
</div>
}

コードの変更が終わったらreload, runします。

zsh
sbt reload
sbt run

conf/evolutions/default/1.sqlで投入した初期ユーザーらが表示されるようになりました。

Step3: ユーザーを登録できる

ユーザーの登録ができるようにDAOにinsert()を追加します。

app/dao/UsersDao.scala
package dao

import scala.concurrent.Future

import javax.inject.Inject
import play.api.db.slick.DatabaseConfigProvider
import play.api.db.slick.HasDatabaseConfigProvider
import scala.concurrent.ExecutionContext
import slick.jdbc.JdbcProfile
import models.User

class UsersDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
extends HasDatabaseConfigProvider[JdbcProfile] {
  import profile.api._

  private val Users = TableQuery[UsersTable]

  def all()(implicit ec: ExecutionContext): Future[Seq[User]] = db.run(Users.result)

+  def insert(u: User)(implicit ec: ExecutionContext): Future[Unit] = db.run(Users += u).map { _ => () }
+
  private class UsersTable(tag: Tag) extends Table[User](tag, "USERS") {
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def age = column[Int]("AGE")

    def * = (id.?, name, age) <> ((User.apply _).tupled, User.unapply)
  }
}

次にControllerを変更します。
ユーザー登録はPOSTメソッドのpayloadを元に行うようにするので、payloadのマッピング(userForm)を定義します。
また、create()を追加し、insert()を呼ぶようにします。

app/controllers/UsersController.scala
package controllers

import scala.concurrent.ExecutionContext
import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request
+import play.api.data.Form
+import play.api.data.Forms.mapping
+import play.api.data.Forms.text
+import play.api.data.Forms.number
import dao.UsersDao
import models.User

@Singleton
class UsersController @Inject()(dao: UsersDao, cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) {

  def index = Action.async { implicit request =>
    dao.all().map {
      u => Ok(views.html.users(u))
    }
  }
+
+  def create = Action.async { implicit request =>
+    val user: User = userForm.bindFromRequest.get
+    dao.insert(user).map(_ => Redirect(routes.UsersController.index))
+  }
+
+  val userForm = Form(
+    mapping(
+      "name" -> text,
+      "age" -> number
+    )((name, age) => User(None, name, age))
+    (u => Some((u.name, u.age)))
+  )
}

Controllerに追加したcreate()POST /usersをマッピングします。

conf/routes
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~

# An example controller showing a sample home page
GET     /                           controllers.HomeController.index()
GET     /users                      controllers.UsersController.index()
+POST    /users                      controllers.UsersController.create()

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

最後にhtmlにユーザー登録用のフォームを作成します。
<div>がハイライトされていますが、Zennのdiff表現の仕様(行の先頭の<が差分の表現だと認識されている)によるもので、実際には差分はありません。

app/views/users.scala.html
-@(users: Seq[User])
+@(users: Seq[User])(implicit request: RequestHeader)
+@import views.html.helper.CSRF

@main("Users") {
<div>
  <div id="users">
+    <h1>Create User</h1>
+    <form action="/users" method="POST">
+      @CSRF.formField
+      <input name="name" type="text" placeholder="name" />
+      <input name="age" type="number" placeholder="age" />
+      <input type="submit" />
+    </form>
    <h1>Users</h1>
    <table>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
      </tr>
      @for(u <- users){
        <tr>
          <td>@u.id</td>
          <td>@u.name</td>
          <td>@u.age</td>
        </tr>
      }
    </table>
  </div>
</div>
}

以上の変更を行ったあとの画面はこうなりました。

フォームにnameとageを入力してsubmitボタンを押して、ユーザー登録ができるようになっています。

ユーザーと企業の関連付け

続いて、ユーザーを登録する際に所属企業も設定できるようにしてみます。

まず企業データを保持するテーブルを作成します。

conf/evolutions/default/2.sql
# --- !Ups

CREATE TABLE "COMPANIES" (
    "ID" INT AUTO_INCREMENT PRIMARY KEY,
    "NAME" VARCHAR NOT NULL,
    "ADDRESS" VARCHAR
);

INSERT INTO "COMPANIES" ("NAME", "ADDRESS") VALUES ('A', 'Tokyo');
INSERT INTO "COMPANIES" ("NAME", "ADDRESS") VALUES ('B', 'Osaka');
INSERT INTO "COMPANIES" ("NAME", "ADDRESS") VALUES ('C', 'Fukuoka');

# --- !Downs

DROP TABLE "COMPANIES";

また、USERSテーブル、UserModelにCompanyIdを追加します。

conf/evolutions/default/3.sql
# --- !Ups

ALTER TABLE "USERS"
ADD COLUMN "COMPANY_ID" INT;

UPDATE "USERS" SET "COMPANY_ID" = 1 WHERE "ID" = 1;
UPDATE "USERS" SET "COMPANY_ID" = 1 WHERE "ID" = 2;
UPDATE "USERS" SET "COMPANY_ID" = 2 WHERE "ID" = 3;

ALTER TABLE "USERS"
ADD CONSTRAINT FK_COMPANY_ID
FOREIGN KEY ("COMPANY_ID") REFERENCES "COMPANIES"("ID");

# --- !Downs

ALTER TABLE "USERS"
DROP CONSTRAINT "FK_COMPANY_ID";

ALTER TABLE "USERS"
DROP COLUMN "COMPANY_ID";
app/models/UserModel.scala
package models

final case class User(
    id: Option[Int],
    name: String,
-    age: Int
+    age: Int,
+    companyId: Int
)

次にCompanyModelを作成します。

app/models/CompanyModel.scala
package models

final case class Company(
    id: Option[Int],
    name: String,
    address: String
)

続いてUsersDaoにCompanyを紐づけてUserを取得するメソッドを作成します。
後でリファクタをしますが、一旦愚直に書いてみます。

app/dao/UsersDao.scala
package dao
import scala.concurrent.Future
import javax.inject.Inject
import play.api.db.slick.DatabaseConfigProvider
import play.api.db.slick.HasDatabaseConfigProvider
import scala.concurrent.ExecutionContext
import slick.jdbc.JdbcProfile
import models.User
+import models.Company

class UsersDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
extends HasDatabaseConfigProvider[JdbcProfile] {
  import profile.api._

  private val Users = TableQuery[UsersTable]
+  private val Companies = TableQuery[CompaniesTable]

  def all()(implicit ec: ExecutionContext): Future[Seq[User]] = db.run(Users.result)

  def insert(u: User)(implicit ec: ExecutionContext): Future[Unit] = db.run(Users += u).map { _ => () }
+
+  def allWithCompany()(implicit ec: ExecutionContext): Future[Seq[(User, Option[Company])]] = {
+    val query = for {
+      (user, company) <- Users joinLeft Companies on (_.companyId === _.id)
+    } yield (user, company)
+    db.run(query.result)
+  }

  private class UsersTable(tag: Tag) extends Table[User](tag, "USERS") {
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def age = column[Int]("AGE")
+    def companyId = column[Option[Int]]("COMPANY_ID")

-    def * = (id.?, name, age) <> ((User.apply _).tupled, User.unapply)
+    def * = (id.?, name, age, companyId) <> ((User.apply _).tupled, User.unapply)
  }
+
+  private class CompaniesTable(tag: Tag) extends Table[Company](tag, "COMPANIES") {
+    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
+    def name = column[String]("NAME")
+    def address = column[String]("ADDRESS")
+
+    def * = (id.?, name, address) <> ((Company.apply _).tupled, Company.unapply)
+  }
}

UsersControllerで呼ぶメソッドをall()からallWithCompany()に変更します。

app/controllers/UsersController.scala
package controllers
import scala.concurrent.ExecutionContext
import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request
import play.api.data.Form
import play.api.data.Forms.mapping
import play.api.data.Forms.text
import play.api.data.Forms.number
+import play.api.data.Forms.optional
import dao.UsersDao
import models.User

@Singleton
class UsersController @Inject()(dao: UsersDao, cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) {

  def index = Action.async { implicit request =>
-    dao.all().map {
-      u => Ok(views.html.users(u))
+    dao.allWithCompany().map {
+      result => Ok(views.html.users(result))
    }
  }

  def create = Action.async { implicit request =>
    val user: User = userForm.bindFromRequest.get
    dao.insert(user).map(_ => Redirect(routes.UsersController.index))
  }

  val userForm = Form(
    mapping(
      "name" -> text,
-      "age" -> number
-    )((name, age) => User(None, name, age))
-    (u => Some((u.name, u.age)))
+      "age" -> number,
+      "companyId" -> optional(number)
+    )((name, age, companyId) => User(None, name, age, companyId))
+    (u => Some((u.name, u.age, u.companyId)))
  )
}

最後にusers.scala.htmlの入力フォームやユーザー一覧の列にCompanyを追加します。
<div>がハイライトされていますが、Zennのdiff表現の仕様(行の先頭の<が差分の表現だと認識されている)によるもので、実際には差分はありません。

app/views/users.scala.html
-@(users: Seq[User])(implicit request: RequestHeader)
+@(data: Seq[(User, Option[Company])])(implicit request: RequestHeader)
@import views.html.helper.CSRF

@main("Users") {
<div>
  <div id="users">
    <h1>Create User here:</h1>
    <form action="/users" method="POST">
      @CSRF.formField
      <input name="name" type="text" placeholder="name" />
      <input name="age" type="number" placeholder="age" />
+      <input name="companyId" type="number" placeholder="companyId" />
     <input type="submit" />
    </form>
    <h1>Users</h1>
    <table>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
        <th>Company</th>
      </tr>
-      @for(u <- users){
+      @for((user, company) <- data){
        <tr>
-          <td>@u.id</td>
-          <td>@u.name</td>
-          <td>@u.age</td>
+          <td>@user.id</td>
+          <td>@user.name</td>
+          <td>@user.age</td>
+          <td>@company.map(_.name)</td>
        </tr>
      }
    </table>
  </div>
</div>
}

ユーザー登録時に企業IDを指定できるようになり、ユーザー一覧にも企業名が表示されるようになりました。

リファクタリング

今の状態ではUsersDaoでcompany関連の定義も持ってしまっています。
例えばCompanyDaoを作成しようとすると、こちらでもCOMPANYテーブル定義情報を持つ必要が出てきます。
テーブル定義情報をまとめて保持するtraitクラスを作成し、各DAOはそれを継承することで責務を分離させてみます。

まず、テーブル定義情報をまとめて保持するTables.scalaを作成します。

app/dao/Tables.scala
package dao

import slick.jdbc.JdbcProfile
import play.api.db.slick.HasDatabaseConfigProvider
import models._

trait Tables extends HasDatabaseConfigProvider[JdbcProfile] {
  import profile.api._

  class UsersTable(tag: Tag) extends Table[User](tag, "USERS") {
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def age = column[Int]("AGE")
    def companyId = column[Option[Int]]("COMPANY_ID")
    def * = (id.?, name, age, companyId) <> ((User.apply _).tupled, User.unapply)
  }

  class CompaniesTable(tag: Tag) extends Table[Company](tag, "COMPANIES") {
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def address = column[String]("ADDRESS")
    def * = (id.?, name, address) <> ((Company.apply _).tupled, Company.unapply)
  }

  val Users = TableQuery[UsersTable]
  val Companies = TableQuery[CompaniesTable]
}

UsersDaoTablesを継承し、不要になった定義を削除します。

app/dao/UsersDao.scala
package dao

import scala.concurrent.Future

import javax.inject.Inject
import play.api.db.slick.DatabaseConfigProvider
import play.api.db.slick.HasDatabaseConfigProvider
import scala.concurrent.ExecutionContext
import slick.jdbc.JdbcProfile
import models.User
import models.Company

class UsersDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
-extends HasDatabaseConfigProvider[JdbcProfile] {
+extends HasDatabaseConfigProvider[JdbcProfile] with Tables {
  import profile.api._
-
-  private val Users = TableQuery[UsersTable]
-  private val Companies = TableQuery[CompaniesTable]

  def all()(implicit ec: ExecutionContext): Future[Seq[User]] = db.run(Users.result)

  def insert(u: User)(implicit ec: ExecutionContext): Future[Unit] = db.run(Users += u).map { _ => () }

  def allWithCompany()(implicit ec: ExecutionContext): Future[Seq[(User, Option[Company])]] = {
    val query = for {
      (user, company) <- Users joinLeft Companies on (_.companyId === _.id)
    } yield (user, company)
    db.run(query.result)
  }
-
-  private class UsersTable(tag: Tag) extends Table[User](tag, "USERS") {
-    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
-    def name = column[String]("NAME")
-    def age = column[Int]("AGE")
-    def companyId = column[Option[Int]]("COMPANY_ID")
-
-    def * = (id.?, name, age, companyId) <> ((User.apply _).tupled, User.unapply)
-  }
-
-  private class CompaniesTable(tag: Tag) extends Table[Company](tag, "COMPANIES") {
-    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
-    def name = column[String]("NAME")
-    def address = column[String]("ADDRESS")
-
-    def * = (id.?, name, address) <> ((Company.apply _).tupled, Company.unapply)
-  }
}

この状態で画面操作をしてみて、これまで通りに操作ができていればリファクタ完了です。

バリデーション

最後にユーザー登録時のバリデーションを実装してみます。
※Form等を使ってそれぞれの項目にバリデーション(e.g. 最大文字長)を定義することもできますが、勉強用に自分で実装してみます。

User.nameについて下記のバリデーションを実施するメソッドを作成します。

  • 英字大文字始まりであること
  • 文字長が256以下であること
/app/controllers/Validations.scala
package controllers

import models.User

object UserValidation {
  def validate(input: User): Either[Seq[String], User] = {
    val errors: Seq[String] = Seq(
      if (!nameStartWithUppercase(input.name)) Some("Name must start with uppercase.") else None,
      if (!nameLessThan256Chars(input.name)) Some("Name must be 256 characters or fewer.") else None
    ).flatten

    if (errors.isEmpty) Right(input)
    else Left(errors)
  }

  def nameStartWithUppercase(name: String): Boolean = {
    name.headOption.exists(_.isUpper)
  }

  def nameLessThan256Chars(name: String): Boolean = {
    name.length <= 256
  }
}

Contollerでバリデーションメソッドを呼ぶようにします。
Rightが返ってきた場合は、これまで通りUsersController.indexにリダイレクトを行い、Leftが返ってきた場合は、エラー文言を表示するようにします。

app/controllers/UsersController.scala
package controllers

import scala.concurrent.ExecutionContext
import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request
import play.api.data.Form
import play.api.data.Forms.mapping
import play.api.data.Forms.text
import play.api.data.Forms.number
import play.api.data.Forms.optional
import dao.UsersDao
import models.User

@Singleton
class UsersController @Inject()(dao: UsersDao, cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) {

  def index = Action.async { implicit request =>
    dao.allWithCompany().map {
      result => Ok(views.html.users(result))
    }
  }

  def create = Action.async { implicit request =>
-    val user: User = userForm.bindFromRequest.get
-    dao.insert(user).map(_ => Redirect(routes.UsersController.index))
+    userForm.bindFromRequest.fold(
+      formWithErrors => {
+        Future.successful(BadRequest("Form input has errors"))
+      },
+      userData => {
+        UserValidation.validate(userData) match {
+          case Right(user) =>
+            dao.insert(user).map(_ => Redirect(routes.UsersController.index))
+          case Left(errors) =>
+            Future.successful(BadRequest(errors.mkString("\n")))
+        }
+      }
+    )
  }

  val userForm = Form(
    mapping(
      "name" -> text,
      "age" -> number,
      "companyId" -> optional(number)
    )((name, age, companyId) => User(None, name, age, companyId))
    (u => Some((u.name, u.age, u.companyId)))
  )
}

画面から、下記データを入力してみます。

  • name: 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567 (257文字)
  • age: 20
  • companyId: 1

正常系も確認して問題なく動作すれば終了です。

まとめ

Scalaの勉強としてPlayを使ってサンプルアプリケーションを作成してみました。特にFutureEitherについては調べたことを記載しきれていないので、時間があれば別途まとめてみようと思います。

GitHubで編集を提案

Discussion