技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Scala から MongoDB へアクセスする Casbah を真面目に使ってみる

MongoDBをScalaで触る準備 - 謎言語使いの徒然

でScalaからMongoにアクセスする為のライブラリはインポートしてみたが、使ってなかったので、改めて使ってみる。

MongoDB のデータ構成

まずどんな階層構造でデータを保持してるのかを確認する。

Getting Started with MongoDB — MongoDB Manual 2.4.8

この辺を確認してみると、MongoDB のデータ厚生は

  • MongoDB collection key -> value ... collection *** ...

てな構成らしい。

これだけ分かれば、DBとして使えなくはない。

Casbah 使ってみる

まずこのクライアント、チュートリアルは1. Tutorial: Using Casbah — Casbah (MongoDB + Scala Toolkit) Documentation 2.6.3 documentationに存在してる。

まずいきなり疑問。この MongoClient ってコネクションプールどうなってるのか?あと、ThreadSafeなのかどうかが問題。

まずクライアントプールについては StackOverflow に答えがあった。

scala - How to pool the connections of mongodb with casbah? - Stack Overflow

出展がほしかったが、、、、。

で、ThreadSafety については、Java 側がスレッドセーフだったのでおそらくスレッドセーフだ。Java Driver Concurrency

まぁ MongoClient がスレッドセーフではないとかそんな狂った話はなかろうさ。 後、データベースアクセサ(MongoDBインスタンス)はスレッド単位でキャッシュすればよさそうだ。そもそもTransactionが無いのだから、単一スレッド上で二つ以上の MongoDB インスタンスが必要とも思えない。

ので、Java の ThreadLocal クラスを利用して、キャッシュしよう。

package models.connections

import com.mongodb.casbah.Imports._

trait DB {
  private val host = "127.0.0.1"
  private val port = 27017
  private val databaseName = "rss_reader"
  private val client = MongoClient(host, port)
  private val threadLocal = new ThreadLocal[MongoDB]

  def db(dbName:String):MongoDB = client(dbName)

  def get() = {
    Option(threadLocal.get()).getOrElse({
      val dbInstance = db(databaseName)
      threadLocal.set(dbInstance)
      dbInstance
    })
  }
}

object DB extends DB

マジックナンバーは設定ファイルにでも書き出せばOKかなと。

しかし、、、、この公式の import やら書き方やらの説明の無さは異常で、実行した際の型説明すらないとストレスがマッハな訳ですが、、、。

突っ込んだデータをMongo内の型だけで一般型のキャストまで書かれてなくてこれでどうやってプログラムを書けというのか...

そっちがその気なら全部調べるしかねーよなぁ!

  • insert
object Sample extends App {
  val col = DB.get().getCollection("test_collection")
  val inserts = MongoDBObject("uid" -> "SomethingId", "provider" -> "FACEBOOK")
  val writeRes = col.insert(inserts)
  println("toString : " + writeRes)
  println("Errors   : " + writeRes.getError)
  println("N        : " + writeRes.getN)
}
/*
toString : { "serverUsed" : "/127.0.0.1:27017" , "n" : 0 , "connectionId" : 14 , "err" :  null  , "ok" : 1.0}
Errors   : null
N        : 0
*/
  • findOne

これが独自型で帰ってきて何をどうとればいいのか意味不明である。

が、どうも toMap するとキーが文字列、中身が Map[Any, Any] で返るっぽい。てか Java のしかもGenericsですらない Map で返ってくるよ。何だこのScalaっぽくない仕様は、、、

object Sample extends App {
  val col = DB.get().getCollection("test_collection")
  val inserts = MongoDBObject("uid" -> "SomethingId", "provider" -> "FACEBOOK")
  val res = col.findOne(inserts)
  println("res  : " + res)
  println("keys : " + res.keySet())
  println("map  : " + res.toMap.getClass)
}

入れたのがStringしか入れていないし、そのままキャストできるかなーとStringにキャストするとエラー

[error] (run-main) java.lang.ClassCastException: org.bson.types.ObjectId cannot be cast to java.lang.String
java.lang.ClassCastException: org.bson.types.ObjectId cannot be cast to java.lang.String
    at models.connections.Sample$$anonfun$1.apply(Sample.scala:34)
    at models.connections.Sample$$anonfun$1.apply(Sample.scala:32)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
    at scala.collection.Iterator$class.foreach(Iterator.scala:727)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1157)
    at scala.collection.IterableLike$class.foreach(IterableLike.scala:72)

何なんだBSONって... もしかして BSON 以外のオブジェクト型で返ってきたりとかするのか!? と思ってBSON調べてみると、

BSON

マジかこれ、、、バイナリJSONでつか、、、、すなわち取得できるのはバイナリであると、、、。

と思いきや、ダメ元でフィールドをひとつづつ get すると

  val inserts = MongoDBObject("uid" -> "SomethingId", "provider" -> "FACEBOOK")
  val res = col.findOne(inserts)
  println("map  : " + res.get("uid").getClass)

// map  : class java.lang.String
  • update
  val col = DB.get().getCollection("test_collection")
  val tgt     = MongoDBObject("uid" -> "SomethingId")
  val update  = MongoDBObject("uid" -> "SomethingId", "provider" -> "FACEBOOK", "age" -> 64)
  val res = col.update(tgt, update)
  println(res)

insert では更新できない。KVSとも違うからある種そういうものなのかも?

で、整数を突っ込んだ後で findOne して get するとどーなるかというと

  val tgt     = MongoDBObject("uid" -> "SomethingId")
  val res = col.findOne(tgt)
  println("res  : " + res)
  println("keys : " + res.keySet())
  println("map  : " + res.get("age").getClass)

結果

res  : { "_id" : { "$oid" : "5292e7410cf249da779c4a85"} , "uid" : "SomethingId" , "provider" : "FACEBOOK" , "age" : 32}
keys : [_id, uid, provider, age]
map  : class java.lang.Integer

一応は拾って来れるのね、、、。

  • find で全件取得

DBCursor 型で返るらしい。java.lang.Iterable, java.lang.Iterator で返るらしい。

iterator メソッド実行して、JavaConversions をインポートすれば foreach 出来る。

  val res = col.find()
  import scala.collection.JavaConversions._
  res.iterator().foreach(value => {
    println("===========================")
    println("res  : " + value)
    println("keys : " + value.keySet())
    println("map  : " + value.get("age").getClass)
  })

で、実行すると

===========================
res  : { "_id" : { "$oid" : "5292ddf30cf275afbf800915"} , "uid" : "SomethingId" , "provider" : "FACEBOOK" , "age" : 64}
keys : [_id, uid, provider, age]
map  : class java.lang.Integer
===========================
res  : { "_id" : { "$oid" : "5292e9700cf2f58bc894e697"} , "uid" : "Undefined" , "provider" : "TWITTER" , "age" : 64}
keys : [_id, uid, provider, age]
map  : class java.lang.Integer

これはなんというか驚く程使いづらい。

JavaSQLドライバでももう少しましな IF してたと思うんだけどなぁ。

ちなみにまともに ObjectMap したければ サードパーティ使えと言ってる。