こんにちは、SI部の藤沢です。
今回は、以前作成した「【初心者向け】Playframework2.3(Scala)でPlay-ReactiveMongoを使ってみた」をベースに名前・メールアドレス・年齢を一覧表示、新規登録、更新、削除する簡単なアプリケーションを作成したいと思います。

まずは、完成形になります。
完成形

アプリケーションの作成手順は以下のようになります。Scala,Play Framework,MondoDBは事前にインストールしておいて下さい。

  1. Play Framework、Play-ReactiveMongoのセットアップ
  2. 一覧表示の作成
  3. 新規登録の作成
  4. 更新の作成
  5. 削除の作成
  6. メッセージの日本語表示とデザイン

Play Framework、Play-ReactiveMongoのセットアップ

コンソールを立ち上げ、下記コマンドを実行してプロジェクトを作成します。テンプレートは「play-scala」を選択します。

> activator new
> play-scala

作成したプロジェクトの直下の ./build.sbt を Play-ReactiveMongo のみの依存ライブラリに修正します。

name := """play-scala"""

version := "1.0-SNAPSHOT"

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

scalaVersion := "2.11.1"

resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"

libraryDependencies ++= Seq(
  "org.reactivemongo"		%% "play2-reactivemongo"		% "0.10.5.akka23-SNAPSHOT"
)

次に、プロジェクトフォルダの conf フォルダに play.plugins というファイルを作成し、Play-ReactiveMongoプラグインの実行優先度とクラスを指定します。

1100:play.modules.reactivemongo.ReactiveMongoPlugin

同じ conf フォルダにある application.conf にMongoDBの接続情報を追記します。

… 中略 …
# MongoDB
mongodb.servers = ["localhost:27017"]
mongodb.db = "mydb"

次にルーティングの設定を行うため routes に設定を記述します。Windowsの場合はこのファイルに日本語のコメントを使用するとコンパイルエラーとなりますのでご注意してください。

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index

# List
GET     /employees                  controllers.Application.list(f ?= "")

# Add
GET    /employees/new               controllers.Application.create
POST   /employees/save              controllers.Application.save

# Update
GET    /employees/:id               controllers.Application.edit(id: String)
POST   /employees/:id               controllers.Application.update(id: String)

# Delete
POST   /employees/:id/delete        controllers.Application.delete(id: String)

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

9行目が一覧表示、12、13行目が新規登録、16、17行目が更新、20行目が削除になります。

ルーティングの設定に対するメソッドを作成するために Application.scala を修正します。

package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {

  def index = Action {
    Ok(views.html.index("Your new application is ready."))
  }

  /**
   * 一覧表示メソッド
   */
  def list(filter: String) = TODO

  /**
   * 新規登録メソッド
   */
  def create = TODO
  def save = TODO

  /**
   * 更新メソッド
   */
  def edit(id: String) = TODO
  def update(id: String) = TODO

  /**
   * 削除メソッド
   */
  def delete(id: String) = TODO
}

前述のルーティングで定義したメソッド全てを定義する必要があるため、暫定的に TODO というメソッドを使用しています。次にこれらのメソッドの実装を行っていきます。
その前に、プロジェクトが起動することを確認します。コンソールを立ち上げ、プロジェクトフォルダに移動し、下記コマンドを実行してプロジェクトを起動します。

> .\activator run

プロジェクト起動後、ブラウザから以下のアドレスにアクセスします。作成したファイルの内容に問題がなければ、以下の画面が表示されます。
http://localhost:9000/
セットアップ

続けて、以下のアドレスにアクセスし、画面が正しく表示されることを確認します。
http://localhost:9000/employees
TODO

一覧表示の作成

前章のセットアップで修正した Application.scala のファイルに実装を追記します。一覧表示のメソッドの前に index と home を実装します。
index は http://localhost:9000 でアクセスした時のメソッドで http://localhost:9000/employees にリダイレクトするメソッドとします。

… 中略 …
object Application extends Controller {
  /**
   * http://localhost:9000 に対するメソッド
   */
  def index = Action { home }
  /**
   * http://localhost:9000 からのアクセスを http://localhost:9000/employees にリダイレクト
   */
  def home = Redirect(routes.Application.list())
… 中略 …

一覧表示の list メソッドを実装します。

package controllers

import play.api._
import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json._
import models._
import models.JsonFormats._
import reactivemongo.api._
import play.modules.reactivemongo.MongoController
import play.modules.reactivemongo.json.collection.JSONCollection

import views.html

object Application extends Controller with MongoController {

  /**
   * http://localhost:9000 に対するメソッド
   */
  def index = Action { home }
  /**
   * http://localhost:9000 からのアクセスを http://localhost:9000/employees にリダイレクト
   */
  def home = Redirect(routes.Application.list())

  /**
   * JSONCollectionを返却するcollectionメソッド
   */
  def collection: JSONCollection = db.collection[JSONCollection]("employees")
  /**
   * 一覧表示メソッド
   */
  def list(filter: String) = Action.async {
    implicit request =>
      val futurePage = filter.length > 0 match {
        case true  => collection.find(Json.obj("name" -> Json.obj("$regex" -> (".*" + filter + ".*")))).cursor[Employee].collect[List]()
        case false => collection.genericQueryBuilder.cursor[Employee].collect[List]()
      }
      futurePage.map(employees => Ok(views.html.list(Page(employees), filter)))
  }

  /**
   * 新規登録メソッド
   */
  def create = TODO
  def save = TODO

  /**
   * 更新メソッド
   */
  def edit(id: String) = TODO
  def update(id: String) = TODO

  /**
   * 削除メソッド
   */
  def delete(id: String) = TODO
}

29行目の employees で使用する Model を作成します。 app フォルダの models フォルダに employee.scala ファイルを作成します。

package models

import reactivemongo.bson.BSONObjectID

/**
 * 登録フォームにマッピングするEmployeeケースクラス
 */
case class Employee(
  _id: BSONObjectID,
  name: String,
  mail: Option[String],
  age: Int
)

object JsonFormats {
  import play.api.libs.json.Json
  import play.api.data._
  import play.api.data.Forms._
  import play.modules.reactivemongo.json.BSONFormats._

  // EmployeeケースクラスとJSONCollectionを自動マッピングする設定
  implicit val employeeFormat = Json.format[Employee]
}

case class Page[A](items: Seq[A])

あとは、表示する html を作成します。html は appフォルダの views フォルダ以下に作成します。一覧表示は list.scala.html になります。(application.scala の list メソッドの Ok)

@(currentPage: Page[Employee], currentFilter: String)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Scala Play MongoDB CRUD - List</title>
  </head>
<body>

<div>
  @helper.form(action = routes.Application.list(), 'role -> "検索") {
  <input type="text" name="f" value="@currentFilter" placeholder="名前">
  <input type="submit" value="検索">
  }
</div>
<hr>
<div>
  @Option(currentPage.items).filterNot(_.isEmpty).map { employees =>
  <table>
    <thead>
      <tr>
        <td>名前</td>
        <td>メールアドレス</td>
        <td>年齢</td>
      </tr>
    </thead>
    <tbody>
    @employees.map { employee =>
      <tr>
        <td>@employee.name</td>
        <td>@employee.mail</td>
        <td>@employee.age</td>
      </tr>
    }
    </tbody>
  </table>
  }
</div>
</body>
</html>

http://localhost:9000 にアクセスすると、リダイレクトされ http://localhost:9000/employees の一覧表示が表示されます。
一覧表示

新規登録の作成

新規登録するページにアクセスするためのリンクを list.scala.html に追記します。

… 中略 …
  <input type="submit" value="検索">
  }
  <!-- 以下を追記 -->
  <a href="@routes.Application.create()">新規登録</a>
</div>
… 中略 …

一覧表示と同様に Application.scala の create と save を実装し html を作成します。

… 中略 …
import play.api.data.Form
import play.api.data.Forms._
import scala.concurrent._
import reactivemongo.bson.BSONObjectID

… 中略 …

  /**
    * 登録フォーム
    */
  val employeeForm = Form(
    mapping(
      "id" -> ignored(BSONObjectID.generate: BSONObjectID),
      "name" -> nonEmptyText,
      "mail" -> optional(email),
      "age" -> number)(Employee.apply)(Employee.unapply)
  )

  /**
   * 新規登録表示メソッド
   */
  def create = Action {
    Ok(views.html.create(employeeForm))
  }

  /**
   * 新規登録メソッド
   */
   def save = Action.async {
    implicit request =>
      employeeForm.bindFromRequest.fold(
        formWithErrors => Future.successful(BadRequest(views.html.create(formWithErrors))),
        employee => {
          val futureUpdateEmployee = collection.insert(employee.copy(_id = BSONObjectID.generate))
          futureUpdateEmployee.map { result => home }
        })
  }
@(employeeForm: Form[Employee])
@import helper._

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Scala Play MongoDB CRUD - Create</title>
  </head>
<body>

<div>
  @form(routes.Application.save()) {
    <fieldset>
      @inputText(employeeForm("name"), '_label -> "名前")
      @inputText(employeeForm("mail"), '_label -> "メールアドレス")
      @inputText(employeeForm("age"), '_label -> "年齢")
    </fieldset>
    <div>
      <input type="submit" value="登録" />
      <a href="@routes.Application.list()">キャンセル</a>
    </div>
  }
</div>
</body>
</html>

一覧表示を表示させると、検索の下に「新規登録」リンクが表示されるので、リンクをクリックし新規登録画面を表示します。

新規登録

この時に、何も入力せずに登録ボタンを押下するとエラーメッセージが表示されるが英語です。あとで、日本語で表示されるようにします。

更新の作成

新規登録と同様に、更新するページにアクセスするためのリンクを一覧表示結果の名前にリンクを list.scala.html に追記します。

… 中略 …
      <tr>
        <!-- 以下を編集 -->
        <td><a href="@routes.Application.edit(employee._id.stringify)">@employee.name</a></td>
        <td>@employee.mail</td>
… 中略 …

同様に Application.scala の edit と update を実装し html を作成します。

… 中略 …
  /**
   * 更新表示メソッド
   */
  def edit(id: String) = Action.async {
    val futureEmployee = collection.find(Json.obj("_id" -> Json.obj("$oid" -> id))).cursor[Employee].collect[List]()
    futureEmployee.map {
      employees: List[Employee] => Ok(views.html.edit(id, employeeForm.fill(employees.head)))
    }
  }

  /**
   * 更新メソッド
   */
  def update(id: String) = Action.async {
    implicit request =>
      employeeForm.bindFromRequest.fold(
        formWithErrors => Future.successful(BadRequest(views.html.edit(id, formWithErrors))),
        employee => {
          val futureUpdateEmployee = collection.update(Json.obj("_id" -> Json.obj("$oid" -> id)), employee.copy(_id = BSONObjectID(id)))
          futureUpdateEmployee.map { result => home }
        })
  }
… 中略 …
@(id: String, employeeForm: Form[Employee])
@import helper._

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Scala Play MongoDB CRUD - Update</title>
  </head>
<body>

<div>
  @form(routes.Application.update(id)) {
    <fieldset>
      @inputText(employeeForm("name"), '_label -> "名前")
      @inputText(employeeForm("mail"), '_label -> "メールアドレス")
      @inputText(employeeForm("age"), '_label -> "年齢")
    </fieldset>
    <div>
      <input type="submit" value="更新" />
      <a href="@routes.Application.list()">キャンセル</a>

    </div>
  }
</div>
</body>
</html>

一覧表示の検索結果の名前がリンクになっているため、リンクをクリックし更新画面を表示します。
更新

削除の作成

今までと同様に、削除するリンクを更新画面に削除ボタンを edit.scala.html に追記します。

… 中略 …
</div>
<div>@form(routes.Application.delete(id)) {  <input type="submit" value="削除">  }</div>
</body>
</html>

Application.scala の delete を実装します。

  /**
   * 削除メソッド
   */  def delete(id: String) = Action.async {
    val futureInt = collection.remove(Json.obj("_id" -> Json.obj("$oid" -> id)), firstMatchOnly = true)
    futureInt.map(i => home)
  }

削除

メッセージの日本語表示とデザイン

今までの実装で動作可能となるが、新規登録時や更新時のエラーメッセージが英語となっています。また、デザインも無くシンプルすぎる画面なので、エラーメッセージの日本語化と簡単なデザインを行います。
まずは、エラーメッセージを日本語で表示されるようにします。日本語で表示されるようにするには application.conf の修正と日本語用のメッセージファイルが必要になります。

# The application languages
# ~~~~~
application.langs="en,ja"

日本語用のメッセージファイルは conf フォルダの直下に messages.ja を作成します。

# --- Constraints
constraint.required=必須
constraint.email=Email

# --- Formats
format.numeric=数字

# --- Errors
error.required=必要な項目が入力されていません。
error.number=数字を入力してください。
error.email=メールアドレスを入力してください。

Bootstrap でデザインします。Play で Bootstrap を使用するには WebJars を使うと便利なので、今回は WebJars を使うこととします。使うには build.sbt と routes を修正します。

libraryDependencies ++= Seq(
  "org.reactivemongo"		%% "play2-reactivemongo"		% "0.10.5.akka23-SNAPSHOT",
  "org.webjars"				%% "webjars-play"				% "2.3.0-2",
  "org.webjars"				%  "jquery"						% "2.1.1",
  "org.webjars"				%  "bootstrap"					% "3.2.0"
)
# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)
GET     /webjars/*file              controllers.WebJarAssets.at(file)

上記を設定すると、下記の html で js,css が読み込むことができます。

<link rel="stylesheet" href="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.css"))">
<link rel="stylesheet" href="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap-theme.min.css"))">
<script src="@routes.WebJarAssets.at(WebJarAssets.locate("jquery.min.js"))"></script>
<script src="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.js"))"></script>

全部のページで同じ設定をするのはメンテナンス性に優れていないため、マスターページを作成しコンテンツ部分のみを各ページで出力するように修正します。

@(title: String)(content: Html)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>@title</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" media="screen" href="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.css"))">
    <link rel="stylesheet" media="screen" href="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap-theme.min.css"))">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
    <style type="text/css">
    body {
      padding-top: 60px;
    }
    </style>
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="@routes.WebJarAssets.at(WebJarAssets.locate("jquery.min.js"))"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.js"))"></script>
  </head>
<body>

@* タイトルバー *@
@titlebar()

<div class="row">
  <div class="container">

@* サイドメニュー *@
@sidemenu()

@* コンテンツ *@
<div class="col-xs-12 col-sm-9 col-md-9 col-lg-9">
  <div class="row">
@content
  </div>
</div>

  </div>
</div>

</body>
</html>

マスターページでタイトルバーとサイドメニューを別ファイルとして読み込むようにしたため、タイトルバーとサイドメニューの html を作成します。

<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand" href="@routes.Application.index()">Scala Play MongoDB CRUD</a>
    </div>
  </div>
</div>
<!-- サイドメニュー -->
<div class="hidden-xs col-sm-3 col-md-3 col-lg-3">
  <div class="row">
    <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
      <ul class="nav nav-pills nav-stacked">
        <li><a href="@routes.Application.list()">一覧</a></li>
        <li><a href="@routes.Application.create()">新規追加</a></li>
      </ul>
    </div>
  </div>
</div>

前章までで作成してきた html を修正します。

@(currentPage: Page[Employee], currentFilter: String)

@main(title = "Scala Play MongoDB CRUD - List") {

<!-- 見出し -->
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
  <h3>一覧</h3>

  @helper.form(action=routes.Application.list(), 'class -> "well form-search", 'id -> "search") {
  <div class="input-group col-xs-6 col-sm-6 col-md-6 col-lg-6">
    <input type="text" id="searchbox" name="f" value="@currentFilter" placeholder="名前" class="form-control" />
    <span class="input-group-btn">
      <input type="submit" id="searchsubmit" class="btn btn-primary" value="検索" />
    </span>
  </div>
  }

<div>
  @Option(currentPage.items).filterNot(_.isEmpty).map { employees =>
    <table class="table table-hover">
      <thead>
        <tr>
          <th>名前</th>
          <th>メール</th>
          <th>年齢</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        @employees.map { employee =>
          <tr>
            <td>@employee.name</td>
            <td>@employee.mail</td>
            <td>@{employee.age}歳</td>
            <td><a href="@routes.Application.edit(employee._id.stringify)" class="btn btn-primary btn-sm">詳細</a>
          </tr>
        }
      </tbody>
    </table>
  }
</div>
}
@(employeeForm: Form[Employee])
@import helper._
@import play.api.i18n.Messages

@main(title = "Scala Play MongoDB CRUD - Create") {

<!-- 見出し -->
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
  <h3>新規追加</h3>

<!-- フォーム -->
  @form(routes.Application.save(), 'class -> "well") {
    <fieldset>
      @inputText(employeeForm("name"), '_label -> "名前", 'class ->"form-control")
      @inputText(employeeForm("mail"), '_label -> "メールアドレス", 'class ->"form-control")
      @inputText(employeeForm("age"),  '_label -> "年齢", 'class ->"form-control")
    </fieldset>
    <div>
      <input type="submit" value="登録" class="btn btn-primary btn-sm" />
      <a href="@routes.Application.list()" class="btn btn-default btn-sm">キャンセル</a>
    </div>
  }
</div>
}
@(id: String, employeeForm: Form[Employee])
@import helper._
@import play.api.i18n.Messages

@main(title = "Scala Play MongoDB CRUD - Edit") {

<!-- 見出し -->
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
  <h3>詳細</h3>

<!-- フォーム -->
  @form(routes.Application.update(id), 'class -> "well", 'id -> "update") {
    <fieldset>
      @inputText(employeeForm("name"), '_label -> "名前", 'class ->"form-control")
      @inputText(employeeForm("mail"), '_label -> "メールアドレス", 'class ->"form-control")
      @inputText(employeeForm("age"),  '_label -> "年齢", 'class ->"form-control")
    </fieldset>
    <div>
      <input type="submit" value="更新" class="btn btn-primary btn-sm" />
      <input type="button" value="削除" class="btn btn-danger btn-sm" id="btnDelete" />
      <a href="@routes.Application.list()" class="btn btn-default btn-sm">キャンセル</a>
    </div>
  }
</div>
}
<!-- 削除フォーム -->
  @form(routes.Application.delete(id), 'id -> "frmDelete") {
  }
  <script type="text/javascript">
  $(function () {
    $("#btnDelete").click(function() {
      $("#frmDelete").submit();
    });
  });
  </script>

以上で本アプリケーションの作成完了となります。

さいごに

今回は例外処理を入れていませんが、Scala,Play Framework2.3,MongoDBを用いて登録、更新、削除及び読み込みを行いました。各メソッドは少ないコードで作成できることが、ご理解いただけましたでしょうか。

最後までご高覧頂きまして有難うございます。