こんにちは。SI部の満石です。

一ヶ月ほど前からScalaを学び始め、PlayframeworkからMongoDBを使ってみるところまでやってみたので、今回はそのコードを紹介します。

ScalaやPlayframework、MongoDBのインストールについてはいろいろと情報があるため説明を省略します。
今回使用するPlayframeworkのバージョンは2.3なので、Typesafe Activatorを使用する点に注意してください。
また、PlayframeworkはJavaとScalaのどちらでも記述できますが、タイトルのとおり今回はScalaを使います。

Playframework(Scala)用のMongoDBプラグイン

Playframeworkの公式ドキュメントを見てみると、ModulesのページにはScalaで扱えるMongoDBプラグインはplay-salatだけが紹介されています。
最初はこのplay-salatを使ってPlayframeworkからMongoDBを使えるようにしてみようと四苦八苦したのですが、Playframeworkの2.3で変化した点等、まだScalaを学び始めて1ヶ月の自分には理解が難しい点が多く、結局動作させることが出来ませんでした。

そこで公式ドキュメントには書かれていないものの、調べていて何度も目にしていたReactiveMongoというMongoDB用のScalaドライバを使ってみることにしました。ReactiveMongoのPlayframework用プラグインはPlay-ReactiveMongoがあり、これを使うことで簡単にPlayframeworkでMongoDBを扱うことが出来ました。

Play-ReactiveMongoのページにはサンプルコードが書かれていますがREST APIを作成するサンプルとなっているため、今回はPlayframeworkを学び始めた人でも理解しやすいよう、出来るだけ余計なものを削ぎ落としたシンプルなサンプルコードを紹介します。

Play-ReactiveMongoのセットアップ

今回はPlayframework 2.3を使うため、コンソールで

$ activator new

を実行し、

play-scala

を選択して、Scalaで記述するためのPlayframeworkの初期セットアップが終わっていることを前提とします。

まず作成したPlayframeworkプロジェクトの直下にある、build.sbtの中身を以下のように書き換え、依存ライブラリをPlay-ReactiveMongoだけにしてしまいます。

name := """play-reactivemongo"""

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という名前のファイルを作成し、以下の1行を記述してPlay-ReactiveMongoプラグインの実行優先度とクラスを指定します。

1100:play.modules.reactivemongo.ReactiveMongoPlugin

そして、同じconfディレクトリにあるapplication.confファイルに以下のようにMongoDBの接続設定を追記します。
今回はローカルPCでデフォルト設定で起動したMongoDBに接続し、データベース名は「mydb」とすることにします。
ちなみにMongoDBは単に mongod コマンドで起動しておくだけで良く、無いデータベースは実行時に無ければ勝手に作成されるため、OracleやMySQLといったリレーショナルなDBでは必要な「あらかじめデータベースやテーブルを作成したりしておく手間」は不要です。

mongodb.servers = ["localhost:27017"]
mongodb.db = "mydb"

サンプルアプリケーションの作成

今回はMongoDBにデータを登録し、登録したレコードを検索するだけの簡単なアプリケーションを作成します。できるだけ簡単にするために必要なエラー処理等は省いているところもあるのでご注意ください。

今回のアプリケーションのメインであるControllerは、app/controllersにあるApplication.scalaを以下の内容に書き換えてしまいましょう。

package controllers

import play.api._
import play.api.mvc._
import play.api.data.Forms._
import play.api.data._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.functional.syntax._
import play.api.libs.json._
import scala.concurrent._
import scala.concurrent.duration.Duration
import play.api.data.Form
import models._
import models.JsonFormats._
import reactivemongo.api._
import play.modules.reactivemongo.MongoController
import play.modules.reactivemongo.json.collection.JSONCollection

// Controllerを継承するだけでなく、MongoControllerもミックスインする
object Application extends Controller with MongoController {

	// 登録フォーム
	val employeeForm = Form[Employee](
		mapping(
			"name" -> nonEmptyText,
			"mail" -> optional(email),
			"age" -> number
		)(Employee.apply)(Employee.unapply)
	)

	// 検索フォーム
	val searchForm = Form[Search](
		mapping(
			"name" -> nonEmptyText
		)(Search.apply)(Search.unapply)
	)

	// JSONCollectionを返却するcollectionメソッド。扱うコレクション(RDBだとテーブルにあたる)名はemployees
	def collection: JSONCollection = db.collection[JSONCollection]("employees")

	// 登録画面の初期表示時に呼ばれるメソッド
	def index = Action {
    	Ok(views.html.index(employeeForm))
  	}

  	// 登録画面の登録ボタン押下時に呼ばれるメソッド
	def register = Action { implicit request =>
		employeeForm.bindFromRequest.fold(
			errors => BadRequest(views.html.index(errors)),
			employee => {
				// MongoDBに登録フォームをそのまま登録(初回にemployeesコレクションが自動生成される)
    			collection.insert(employee)
    			// ユーザー情報画面に遷移
				Ok(views.html.result(employee))
			}
		)
	}

	// 検索画面の初期表示時に呼ばれるメソッド
	def search = Action {
    	Ok(views.html.search(searchForm))
  	}

  	// 検索画面の検索ボタン押下時に呼ばれるメソッド
	def findByName = Action { implicit request =>
		searchForm.bindFromRequest.fold(
			errors => BadRequest(views.html.search(errors)),
			search => {
				// 名前で検索
				val cursor: Cursor[Employee] = collection.
      				find(Json.obj("name" -> search.name)).
      				cursor[Employee]

      			// CursorをFutureに変換
      			val futureEmployeeList: Future[List[Employee]] = cursor.collect[List]()

      			// futureEmployeeListからEmployeeのListを取得
      			val employees = Await.result(futureEmployeeList, Duration.Inf)

      			// ユーザー情報画面に遷移(検索結果が0件だとエラーなので注意)
      			Ok(views.html.result(employees.head))

			}
		)
	}
}

次に、もう1つのキモであるModelを作成します。appディレクトリの下にmodelsディレクトリを作成し、その中にModel.scalaファイルを作成します。ファイルの内容は以下を記述します。

package models

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

// 検索フォームにマッピングするSearchケースクラス
case class Search(name: String)

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

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

あとは必要なscala.htmlファイルをapp/viewsディレクトリに作成します。

index.scala.html

@(employeeForm: Form[Employee])
@import helper._
<html>
	<head>
		<title>ユーザー登録</title>
	</head>
	<body>
	<h1>ユーザー登録</h1>
	@helper.form(routes.Application.register) {
		<fieldset>
			@inputText(employeeForm("name"), '_label -> "名前")
			@inputText(employeeForm("mail"), '_label -> "メールアドレス")
			@inputText(employeeForm("age"), '_label -> "年齢")
		</fieldset>
		<input type="submit" class="btn primary" value="登録">
	}
	</body>
</html>

search.scala.html

@(searchForm: Form[Search])
@import helper._
<html>
	<head>
		<title>ユーザー検索</title>
	</head>
	<body>
	<h1>ユーザー検索</h1>
	@helper.form(routes.Application.findByName) {
		<fieldset>
			@inputText(searchForm("name"), '_label -> "名前")
		</fieldset>
		<input type="submit" class="btn primary" value="検索">
	}
	</body>
</html>

result.scala.html

@(employee: Employee)
@main("Result") {
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
    <h1>ユーザー情報</h1>
    <br>
    <h3>名前</h3>
    <p>
    @employee.name
    </p>
    <h3>メールアドレス</h3>
    <p>
    @employee.mail
    </p>
    <h3>年齢</h3>
    <p>
    @employee.age
    </p>
</body>
</html>
}

最後に、conf/routesファイルにControllerのメソッドとリクエストパスとのルーティングを追記します。

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

# Home page
GET     /                           controllers.Application.index
#追記したのはここから
POST    /register                   controllers.Application.register
GET     /search                     controllers.Application.search
POST    /findByName                 controllers.Application.findByName
#ここまで
# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)

サンプルアプリケーションの実行

$ activator run

でPlayframeworkを起動したあと、
http://localhost:9000/
で登録画面、
http://localhost:9000/search
で検索画面が表示されます。

さいごに

Playframeworkの基本的なコードにほんの少しだけReactiveMongoのコードを足すだけで簡単に使えることがご理解いだだけましたでしょうか。
play-salatを使いこなせずに困っている方はぜひPlay-ReactiveMongoを使うことをお勧めします。
今回はシンプルなWebアプリケーションを作成しましたが、ReactiveMongoはJSONとの親和性が高くREST APIでもとても使いやすいようなので、AngularJSのようなクライアントサイドフレームワークと組み合わせて使ってみるのも面白そうです。