そろそろRuby会議もあるというのに、そういや最近Ruby全然書いてないfaultierですこんばんわ。じゃあリハビリを兼ねて久々にRamazeさんで遊んでみるか、ついでにいい加減NoSQLブームにも乗ってみるか、みたいな感じでRamaze+MongoDB+Candyで遊んでみることにした。ちなみに社内では今Cassandraがブームなのだけども、Cassandraはちょっと遊びで使ってみるにはオーバースペックだよなーとか思いつつ色々見てたら、HerokuでMongoDBが使えるらしいのでそれを狙ってのMongoDBいじり。

とりあえず入れてみる

aptで探したらmongodbのパッケージもあるんだけど、2010年5月30日時点では1.2.2と若干バージョンが古い。この後オブジェクトマッパーを色々試してみたところ1.4系じゃないと動かなかったりしたので、本家のサイトから最新のバージョン落としてくる。コンパイル済みのパッケージなので展開して適当なとこに置いとくだけでOK。手元の環境はUbuntu 10.04の64bitなのでLinux 64bitってやつを選んだけど、OSX 32/64 bitとかWindows 32/64 bitとかもあった。

$ mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
>

ふむ。繋った繋った。このあとチュートリアルとかにある例通りちょこちょこいじってみたけどちゃんと使えるようだ。

Rubyから使う

Rubyのdriverはgemからインストールできるので普通にgem install mongoとかやる。依存モジュールでbsonてのが入るけどなんじゃろ、と思ったんだけど、RubyのオブジェクトをMongoDBのBSONオブジェクトにシリアライズするモジュールだけ別のパッケージにしてあるらしい。んで、bsonはC拡張があって、そっちを入れてないとこんなメッセージが出る。

rb(main):001:0> require 'mongo'

**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance.
  You can install the extension as follows:
  gem install bson_ext

  If you continue to receive this message after installing, make sure that the
  bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.

=> true

まぁローカルの開発環境だと別にどっちでもいいんだけど、サーバに置くときはどうせ使うだろうしbson_extも入れておく。

# coding: utf-8
require 'mongo'

con   = Mongo::Connection.new # 何もオプション指定しないと localhostの28017ポートに繋ぐ
db    = con.db('candy')
udons = db.collection('Udon')
udons.insert({
  'name'  => 'ぶっかけ',
  'type'  => '冷たいうどん',
  'price' => 350
})
udons.insert({
  'name'  => 'かけ',
  'type'  => '温かいうどん',
  'price' => 300
})
udons.insert({
  'name'  => '釜玉',
  'type'  => 'かまあげうどん',
  'price' => 400
})

p db.collection_names  #=> [ 'system.indexes', 'Udon' ]
p udons.count          #=> 3
p udons.find_one       #=> {"_id"=>{"$oid"=>"4c0273712e119e3fd7000001"}, "name"=>"ぶっかけ", "type"=>"冷たいうどん", "price"=>350}
p udons.find('price' => {'$gte' => 350}).count #=> 2

こんな感じでさくっと使えた。ほー。あとはMapReduceとか試してみようかと思ったけどまぁそれはおいおい。

オブジェクトマッパー

ぶっちゃけmongo-ruby-driverだけでも十分色々できるし、O/Rマッパー…あーいやRelationじゃないから、O/Dマッパーか、は無くてもいいかなと思ったけども、Ramazeとかで使うことを考えたらやっぱりあった方がいい(工夫してやれば既存のヘルパーとか使えそうなので)。んでちょろっと見てみたら、結構色々あるんだけど、どれもARっぽいんだよなぁ。なんというか、ちょっと過剰なのでもっと薄いのがいい。そしてMongoMapperもMongoidもActiveSupportとかvalidation用のライブラリとか入れちゃう。Railsで使うんならいいんだけど、というかRailsでARの代わりに使うのを想定してるっぽくて(ActiveModelってのがあるんだね。最近Rails全然見てないので知らないんだけどRailsもModel抽象化の流れなんかな)、悩ましい。つーかActiveSupportは入れないで欲しいなぁ…。

個人的には一覧の最後にあったCandyが良さげに見えた。なにしろ「ゴールはActiveRecordやDataMapperのミラーじゃない」ってREADME冒頭で明言してるし、mongo-ruby-driver以外には依存してないのが素敵。使い方もCandy::Pieceをincludeするだけとシンプル。

# coding: utf-8
require 'candy'

class Udon
  include Candy::Piece
end

kamaage = Udon.new
kamaage.name = 'かまあげ'
kamaage.type = 'かまあげうどん'
kamaage.price = 350

ちなみに、何も指定しなければこれでlocalhostの28017ポートに繋いで、candyというデータベースのUdonコレクションにアクセスする。つまりこのコードだと、一個前のコードと同じデータベースの同じコレクションから取ってくることになる。これはCandyクラスかUdonクラスのクラスメソッドで参照/変更できる。Candyクラスで設定すると全てのデフォルト設定になるし、クラスメソッドで設定するとそのクラスのデフォルト設定になる。Ramazeだったら、model/init.rbあたりでこう書いとく。

# coding: utf-8

require 'candy'

Candy.host = "mongdb.example.com"
Candy.port = "24423"
Candy.db   = "udonapp"

require __DIR__('udon')

Udon.collection = 'menrui' # クラス名じゃないコレクション名にしたい場合

あとsaveメソッドはない。なんかこれもポリシーらしくて、オブジェクトを操作したら即時反映される。method_missingでフックして随時更新をかけてるので、もしvalidationとかしたい場合は、例えばpriceが数字でなければならなくて、1000円超えるようなお高いうどんは売っちゃいけないポリシーにするとしたら、こんな感じのメソッドを生やしてやる。superが肝。

class Udon
  include Candy::Piece

  def price=(val)
    raise '値段は数字だっつってんだろ' unless val.kind_of?(Integer)
    raise 'たけーよアホか' unless val < 1000
    super
  end
end

ちなみにバリデーションとかエラーハンドリングとかnamed scopeとかの簡単な実装例はCandyのドキュメントに載ってるので見てみるといいと思う。まぁ若干面倒ではあるけど、正直こんなのでいいよなーと思った。しばらくこれで遊んでみよう。

…というあたりまで書いて大変致命的なことに気付いたんだけど、Candyは1.9系じゃないと駄目らしいんだけどHerokuってRuby 1.9系だっけ…?多分違うよね…?うーん、まぁ、ちょうど借りてるサーバもリビルドしたことだし、とりあえずそのサーバで動かせればいいか…。