Takuya71 のぶろぐ

外資系ソフトウェア会社で働いてます、認定スクラムマスター

play2 + mongodb で チュートリアルの todo list 作成してみる

play2 + mongodb を使う

play2 で mongodb がSQLなデータベースと同じように使えるのか チュートリアルをもとに確認してみました。
題材としては チュートリアルにある TODO Listの DBを mongodb にしてみただけというものです。

play2 で mongodb 使う leon/play-salat.g8 という gitter のひな形があるので、
そちらを使ってひな形は さくっと作りました。
salat は CasbahのMongoDBObjectとscalaのケースクラスと相互変換してくれる、ORマッパーらしいです。

g8実行
% g8 leon/play-salat.g8
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8

This template generates a Scala play 2.0 project with the salat 
plugin 

verbatim [*.html *.js *.png]: 
database_name [my_salat_app]: todolist
database_host [127.0.0.1]: 
application_name [my_salat_app]: play2mongotodo       
play_version [2.0.2]: 2.0.3
salat_plugin_version [1.0.9]: 

Applied leon/play-salat.g8 in .

play のバージョン は 2.0.3 にしました。

play 実行
% play
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8
[info] Loading project definition from /Users/takuya/Dropbox/projects/play2mongotodo/project
[info] Set current project to play2mongotodo (in build file:/Users/takuya/Dropbox/projects/play2mongotodo/)
       _            _ 
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/ 
             
play! 2.0.3, http://www.playframework.org

> Type "help play" or "license" for more information.
> Type "exit" or use Ctrl+D to leave this console.


[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn] 	::          UNRESOLVED DEPENDENCIES         ::
[warn] 	::::::::::::::::::::::::::::::::::::::::::::::
[warn] 	:: net.liftweb#lift-json_2.9.1;2.5-SNAPSHOT: not found
[warn] 	::::::::::::::::::::::::::::::::::::::::::::::
[error] {file:/Users/takuya/Dropbox/projects/play2mongotodo/}play2mongotodo/*:update: sbt.ResolveException: unresolved dependency: net.liftweb#lift-json_2.9.1;2.5-SNAPSHOT: not found

あれっ
net.liftweb#lift-json_2.9.1;2.5-SNAPSHOT: not found
と怒られました。
どうやら resolver の定義が必要なようですね。

project/Build.scala で resolver の設定を行います。

val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
      routesImport += "se.radley.plugin.salat.Binders._",
      templatesImport += "org.bson.types.ObjectId",
      resolvers += "Sonatype" at "https://oss.sonatype.org/content/repositories/snapshots"
    )

改めて play 実行

play2mongotodo] $ run

[info] Updating {file:/Users/takuya/Dropbox/projects/play2mongotodo/}play2mongotodo...
[info] Resolving org.hibernate.javax.persistence#hibernate-jpa-2.0-api;1.0.1.Fin                                                                                [info] Done updating.                                                        
--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on port 9000...

(Server started, use Ctrl+D to stop and go back to the console...)

今度は無事起動しました。

無事起動出来ることが確認出来たので TODO LIST の作成に移りたいと思います。

前提として mongodb はすでにインストールされているものとします。

今回の leon/play-salat.g8 を元にして作った場合、
mongodb の接続定義は g8 で作成するときに指定しますが、
後から設定/変更する場合は
conf/application.conf ファイルの中にある

mongodb.default.db = "todolist"
# Optional values
#mongodb.default.host = "127.0.0.1"
#mongodb.default.port = 27017
#mongodb.default.user = "leon"
#mongodb.default.password = "123456"

この部分を変更します。

Todo list の作成

ここから先は Todo list の作成

ルーティングの定義

/conf/route
# Home page
GET     /                           controllers.Application.index

# Tasks
GET     /tasks                      controllers.Application.tasks
GET     /view/:id                   controllers.Application.view(id: ObjectId)
POST    /tasks                      controllers.Application.newTask
POST    /tasks/:id/delete           controllers.Application.deleteTask(id: ObjectId)

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

ここは チュートリアルとほぼ変化ありません。
view に渡す引数のid は ObjectId 型を渡してます。

コントローラ

/app/controllers/Application.scala
package controllers

import play.api._
import play.api.mvc._
import models.Task
import se.radley.plugin.salat._
import com.mongodb.casbah.Imports._
import com.novus.salat._
import play.api.data._
import play.api.data.Forms._

object Application extends Controller {
  val taskForm = Form(
    "label" -> nonEmptyText
  )

  def index = Action {
    Redirect(routes.Application.tasks)
  }

  def tasks() = Action {
    val tasks = Task.findAll
    Ok(views.html.list(tasks.toList,taskForm))
  }

  def newTask = Action { implicit request =>
    taskForm.bindFromRequest.fold(
      errors => BadRequest(views.html.list(Task.findAll.toList,errors)),
      label => {
        val task = Task(label=label)
        Task.save(task)
        Redirect(routes.Application.tasks)
      }
    )
  }

  def deleteTask(id: ObjectId) = Action {
    Task.removeById(id)
    Redirect(routes.Application.tasks)
  }

  def view(id: ObjectId) = Action {
    Task.findOneById(id).map( task =>
      Ok(views.html.task(task))
    ).getOrElse(NotFound)
  }
}

基本的に ほぼ同じですが、
一覧の取得に findAll を使っています。こちらは結果が Iterator で返ってくるので
Listに変換しています。
newTask の部分でも レコードの保存では save メソッドを実行して保存しております。

model

次は model の作成
/app/models/User.scala は削除し、
/app/models/Task.scala 作成しました。

/app/models/Task.scala
package models

import play.api.Play.current
import java.util.Date
import com.novus.salat._
import com.novus.salat.annotations._
import com.novus.salat.dao._
import com.mongodb.casbah.Imports._
import se.radley.plugin.salat._
import salatcontext._

case class Task(
                 id: ObjectId = new ObjectId,
                 label: String,
                 added: Date = new Date(),
                 updated: Option[Date] = None,
                 deleted: Option[Date] = None
                 )

object Task extends ModelCompanion[Task, ObjectId] {
  val dao = new SalatDAO[Task, ObjectId](collection = mongoCollection("tasks")) {}
}

こちらは チュートリアルよりも 関数定義が減ってます。
ModelCompanion を extend しているので mongodb にある操作系の関数はそのまま結構つかえそうです。
findAll や save は定義してなくても使えるのは extend しているからですね。

object Task extends ModelCompanion[Task, ObjectId] {
val dao = new SalatDAO[Task, ObjectId](collection = mongoCollection("tasks")) {}
}

この dao として定義している箇所で、mongodb のテーブルにあたる collection に接続する為の定義となります。この例ですと tasks collection にこのオブジェクトは接続することになります。

view

次は view です。
/app/views/index.scala.html
/app/views/welcome.scala.html
は削除しました。

/app/view/list.scala.html 作成
@(tasks: List[Task], taskForm: Form[String])

@import helper._

@main("Todo list") {
<h2>@tasks.size task(s)</h2>
<ul>
    @tasks.map { task =>
    <li>
        <a href="@routes.Application.view(task.id)">@task.label</a>
        @form(routes.Application.deleteTask(task.id)) {
        <input type="submit" value="Delete">
        }
    </li>
    }
</ul>
<h2>Add a new task</h2>

@form(routes.Application.newTask) {
@inputText(taskForm("label"))
<input type="submit" value="Create">
}
}
/app/view/task.scala.html 作成
@(task:Task)

@main("Task Detail") {
<h1>This is @task.label task</h1>
<p>This was added on the @task.added.format("dd MMM yyyy")</p>
}

ビューも チュートリアルと変わりません。

これだけで DB として mongodb を使うようにすることが出来ました。
実際違うところは Application.scala の一部のメソッドとmodelの定義ぐらいでした
これも salat のおかげです。

全体はGithubにて公開しております。