Play framework 2.0でMongoDBを使ってみる

普段はMySQLか、もしくはPostgreSQLな仕事ばかりでMongoDBを使うようなことは無かったので、一度MongoDBを触ってみようと思い、記事の作成とそこにコメントを付けていくようなアプリを書いてみた。ログインはめんどいので割愛。
play-mongo-sample

ODM

MongoDBへのアクセスには、Salatというscalaのケースクラスとの変換をやってくれるORMならぬODMを使用。Play framework(scala)からはプラグインのplay-salatを使うと簡単に扱えるようだ。1.0.9だとライブラリ依存関係の解決でエラーが出てしまう(https://github.com/leon/play-salat/issues/28)ので、1.1-SNAPSHOTで。

記事の構造

MongoDBだと埋め込みオブジェクトが持てるので、記事にコメントのオブジェクトを埋め込むような形で扱うことにする。こんな感じ。

{
  title: "タイトル",
  body:  "本文",
  comments: [
    { body: "コメント1" },
    { body: "コメント2" }
  ]
}

Model

modelsパッケージにPost.scalaを作成し、記事の構造を定義したケースクラスと記事の操作を書いていく。
ケースクラスはこんな感じ。

case class Comment(
  body: String
)
case class Post(
  @Key("_id")id: ObjectId = new ObjectId,
  title: String,
  body: String,
  comments: List[Comment] = List()
)

SalatのModelCompanion traitを使うと基本的な操作が提供されるようで便利。さらにそこへコメントの追加処理などを定義していく。$pushを使ったupdateのMongoDBObjectの入れ子は面倒。。。

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

  def findOneById(id: String): Option[Post] = {
    try {
      dao.findOneById(new ObjectId(id))
    }catch {
      case e: IllegalArgumentException => None
      case _ => None
    }
  }
  
  def addComment(p: Post, comment: Comment) = {
    update(
        MongoDBObject("_id" -> p.id),
        MongoDBObject(
            "$push" -> MongoDBObject(
                "comments" -> MongoDBObject(
                    "body" -> comment.body
                )
            )
        ),
        false, false, new WriteConcern)
  }

}

controller, form

controllers/Posts.scalaに必要なアクションを定義し、routesに追加。本当は記事内容の修正や削除なんかも必要だけど、記事の作成と埋め込みオブジェクトのコメントの追加のところをやってみたかっただけなので割愛。あと、ところどころかなりいい加減。エラー処理とか。
最初Formをこんな風に定義していて、ちょっとはまってしまった。

val postForm = Form(
  mapping(
    "id" -> ignored(new ObjectId),
    "title" -> nonEmptyText,
    "body" -> nonEmptyText
  )(Post.apply)(Post.unapply)
)

これだとObjectIdが重複して2件目以降の保存が失敗してしまったので、次のように修正。

val postForm = Form(
  mapping(
    "title" -> nonEmptyText,
    "body" -> nonEmptyText
  )
    ((title: String, body: String) => Post(title = title, body = body))
    ((post: Post) => Some(post.title, post.body))
)

とりあえずPlay frameworkで触ってみたけど、アプリからMongoDBを使うところに関してはそれほど難しくなさそうな感じ。オブジェクトとのマッピングに関してはSQLよりもよっぽど自然にできそうだし。他の言語でも有名どころでは大体ODMが揃ってるみたいだし。

データモデリングに関しては、1対多の関係を配列で埋め込むようにしたり、多対多の関係をそれぞれIDの配列を持ち合うような形を基本として考えて、そこから検索の都合やデータ量の都合などが要件に合うように崩していくような感じでいいんだろうか。

参考

MongoDBの薄い本(The Little MongoDB Book)

短いチュートリアルの中に色々重要なことがまとまってるように感じました。

MongoDBにおける関連(Relation)のスキーマ設計

MongoDBでのデータモデリングについて。ここでいうパターン2やパターン4を多用できたほうがMongoDBでは扱いやすそう。