ScalaからMongoDBへアクセスする - Salat編

March 9, 2011 - Scala

前回はCasbahというライブラリを使ってMongoDBへアクセスしてみました。

ScalaからMongoDBへアクセスする ? Casbah編

今回は、Casbahに加えて、Salatというライブラリをを組み合わせて、より便利にMongoDBとScalaとやりとりをする方法について見ていきます。

Salat

novus/salat - GitHub

Salatは、CasbahのMongoDBObjectとscalaのケースクラスと相互変換してくれる、ORマッパーです。wikiから引用するとこうあります。

Salat is a bi-directional Scala case class serialization library that leverages MongoDB’s DBObject (which uses BSON underneath) as its target format. This project is focused on fostering a DWIM and intuitive usage pattern for the end-user’s benefit, without sacrificing run time performance.

パフォーマンスの犠牲なしに、より便利にMongoDBとやりとりできるというものらしいです。DWIMって初耳なんですが、”Do What I Mean.“の略語だそうです。意図したとおりに動いてくれる、くらいの意味でしょうか。 あと、「Salat」って、ロシア語で「サラダ」の意味だそうです。

では、Salatの簡単な使い方を見ていきます。

インストール

今回もsbt前提です。 CasbahとSalatどちらも必要です。今回はCasbahは2.0.2を、Salatは0.0.5を利用します。あとはいつものように”sbt reload update”を実行するだけです。

CasbahでMongoDBへアクセス

まずはCasbaとSalatをインポート。

import com.novus.salat._
import com.novus.salat.global._
import com.mongodb.casbah.Imports._

CasbahのみでMongoDBに入れる例を復習します。

val collection = MongoConnection()("salat_test")("sample")
collection += MongoDBObject("id"->1, "name"->"me", "age"->27)
println(collection.findOne( MongoDBObject("id"->1)).get )
// { "_id" : { "$oid" : "4d76475ce10d23dcda26857d"} , "id" : 1 , "name" : "me" , "age" : 27}

で、このような値がすでにMongoDBに入っているとして、それぞれのフィールドにアクセスする場合、以下のようになります。

val me2 = collection.findOne( MongoDBObject("id"->1)).get
println( me2.getClass ) // class com.mongodb.BasicDBObject
println( me2.get("name") ) // me

最後の”me2.get(“name”)“というのが格好悪いですね。もしかしたらDBから取り出したときに”name”というキーが存在しないかもしれません。ということで、そのフォーマットをケースクラスで定義できるSalatの出番です。 Salatでは、grater[]のインスタンスを用いてMongoDBObjectとケースクラスの変換を行います。シリアライズは DBに保存する際は「asDBObject」メソッドで取り出してクラスインスタンスとして扱う場合は「asObject」メソッドを使います。

まずはクラスインスタンスをDBに入れる例。

Userというケースクラスを定義して、graterを用いてDBObjectに変換しています。

case class User(id: Int, name: String, age: Int)

val me = User(id=2, name="me2", age=54)
val g = grater[User]
collection += g.asDBObject(me)

つぎに、DBからとりだした値をクラスインスタンスに変換する例です。

val meInDB = collection.findOne( MongoDBObject("id"->2)).get
println( meInDB.getClass )
// class com.mongodb.BasicDBObject
println( meInDB )
// { "_id" : { "$oid" : "4d764830e10d23dc4758c29a"} , "_typeHint" : "User" , "id" : 2 , "name" : "me2" , "age" : 54}
println( g.asObject(meInDB) )
// User(2,me2,54)

このように、ケースクラスへのマッピングが行われることにより、Scalaのコード中で扱うMongoDBのドキュメントの型が明確になり、見通しがよくなります。

「asObject(meInDB)」で変換する前の、DBからとりだしたままの状態(BasicDBObject型)の段階で

“_typeHint” : “User”

というキーと値が見えます。この値がこのドキュメントに対応するクラス型を定義することになります。

問題点:意図しないケースクラスへの変換

別のケースクラスに変換しようとするとどうなるでしょうか。

case class UserA(id: Int, name: String, age: Int)
println( grater[UserA].asObject(meInDB) ) // UserA(2,me2,54)
case class UserB(id: Int, name: String, salary: Int)
println( grater[UserB].asObject(meInDB) )
// java.lang.Exception: class UserB requires value for 'salary'

クラス名が違うUserAに変換しようとすると変換できてしまいます。キー名が異なるUserBに変換しようとした場合はコンパイルエラーとなります。「_typeHint」の値がうまく機能しているのか、少々疑問が残ります…

問題点:クラス階層の保持

また、クラス階層をもつ場合がテストケースにあるのですが、これも微妙な挙動をします。

salat-core/src/test/scala/com/novus/salat/test/model/TestModel.scala at master from novus/salat - GitHub

上記はテストケース用のモデル定義なのですが、Desmondのように別クラスの型を保持するケースクラスをDBObjectに変換する際に、Desmond型は保持できるのですが、それに含まれるAlice型が保持できずにリストに変換されてしまいます。

こんな感じのコードで試してみました。

case class Group(group_id: Int, name: String, leader: User, members: List[User])

val me = User(id=11, name="me", age=27)
val you = User(id=12, name="you", age=30)
val members = MongoDBList.newBuilder
members += me
members += you
val group = Group(group_id=1, name="you and me", leader=me, members=List(me, you))
println(group)
// Group(1,you and me,List(User(11,me,27), User(12,you,30)))

println(grater[Group].asDBObject(group))
// // { "_typeHint" : "Group" , "group_id" : 1 , "name" : "you and me" , "leader" : [ 11 , "me" , 27] , "members" : [ [ 11 , "me" , 27] , [ 12 , "you" , 30]]}

DBObjectに変換すると、leaderの値やmembersのList要素がリストに変換されてます。 このままMongoDBに保存して、それを取り出したあとで”grater[Group].asObject(group)“しようとするとエラーになります。

まだ触り始めたばかりでGithubのWikiもちゃんと読み込んでないので、扱い方が違っているのかもしれません。引き続き動作検証をすすめたいと思います。(結局、Rogueを使うというオチになりそうな気がしないくもないですが…)

関連リンク

今回も一応全コードを掲載しておきます。