2010年08月09日のツイート

Rails3 対応 MongoDB ORM、Mongoid 詳解―前説

つい先日 1.6.0 がリリースされ、MongoDB の時代がいよいよキタ!って感じです。MongoDB 自体のインストール・操作などは下記の記事を参考にしてください。


この記事では、Rails3 にふさわしい ORM として、Mongoid を紹介します。


MongoDB の Ruby ドライバーは、mongo ですが、これは素のドライバーで、Rails などと使用するときは、クラス・オブジェクトに自動でマッピングしてくれる ORM を使用したいところです。

その候補として、

があります、

この内、プロダクションで使われているのは、MongoMapper と Mongoid です。Rails2 では、MongoMapper がよく使われていて、他のプラグインの対応も良かったようですが、Rails3 においては対応が遅れ気味ですし、インターフェイスも Rails2 時代の古いものです。最大の弱点はドキュメントが貧弱なことです。

その点、Mongoid は、Rails3 対応に向けて活発に開発が進んでおり、インターフェイスも Rails3 の AcitiveRecord のベースになっている Arel 風味ですし、公式サイトでのドキュメントの充実ぶりは中々のものです。その上 Devise, CarrierWave といった Rails 定番プラグインも対応しているので、私は Rails3 向けには断然、Mongoid をオススメします。

Sinatra などの軽いフレームワークには、mongo にしか依存していない Candy がいいかもしれません。ただし、Candy は Ruby 1.9 以上対応なので気をつけてください。

というわけで、これから、公式サイトのドキュメントを翻訳し、若干の解説を行ないます。

Rails3 対応 MongoDB ORM、Mongoid 詳解―インストール

まずは、インストールの解説です。

$ gem install mongoid --pre

Rails3 対応の最新版は執筆時点で 2.0.0.beta.16 で、ベータ版なので、--pre オプションを使ってインストールします。2.0.0 リリースの際には、--pre を取って下さい。

そして、mongo ドライバー自体のパフォーマンスを上げるために、bson_ext をインストールしましょう。

$ gem install bson_ext

bson_ext には、boost が必要です。MongoDB のインストール時にインストールされていると思いますので、問題ないと思います。もし、インストールできなくても動きますので、気にしないで下さい。

Rails 3 でのインストール

Rails3 は Bundler を使用しており、必要な Gem を、Gemfile というファイルに書くことになります。

gem "mongoid", "2.0.0.beta.16"
gem "bson_ext", "1.0.4"

2.0.0 リリースの暁には、バージョンの部分を描き直してください。

$ bundle install vendor/bundle

で、vendor/bundle 以下に、Mongoid や依存している Gem がインストールされ、Rails が使用します。Git を使用している方は、.gitignore に vendor/bundle を記入するのを忘れないようにしましょう。

Gemfile を書き換えた後、アップデートをかけるには、

$ bundle update

とします。

設定

Mongoid の設定をするためには、次のコマンドを打ちます。

$ rails generate mongoid:config

すると、config/mongoid.yml が生成され、デフォルトの ORM が Mongoid になるので、rails generate model Foo などとした際に、Mongoid のモデルが作成できるようになります。

それでは、config/mongoid.yml を、自分の環境に合わせて書き換えましょう。

defaults: &defaults
  host: localhost
  slaves:
    - host: slave1.local
      port: 27018
    - host: slave2.local
      port: 27019
  autocreate_indexes: false
  allow_dynamic_fields: true
  include_root_in_json: false
  parameterize_keys: true
  persist_in_safe_mode: false
  raise_not_found_error: true
  reconnect_time: 3

development:
  <<: *defaults
  database: hoge_development

test:
  <<: *defaults
  database: hoge_test

# set these environment variables on your prod server
production:
  <<: *defaults
  host: <%= ENV['MONGOID_HOST'] %>
  port: <%= ENV['MONGOID_PORT'] %>
  username: <%= ENV['MONGOID_USERNAME'] %>
  password: <%= ENV['MONGOID_PASSWORD'] %>
  database: <%= ENV['MONGOID_DATABASE'] %>

host はそのままですね、接続する MongoDB が動いているホストです。
port が省略されていますが、デフォルトでは 27017 番になります。
slaves の項は、スレイブのサーバーがある時です、開発用にはないと思うので、削除したら良いです。

その他のパラメーターは以下を意味します。

autocreate_indexes デフォルトは false です。true にした場合は、モデルクラスがロードされた際に毎回インデックスが付与されます。 development や test 以外の環境では推奨されません。
allow_dynamic_fields モデルクラスにフィールドとして設定されてない属性が呼び出されたときに、オブジェクトに属性を付け加えます。デフォルトは true で、false にした場合は、フィールドとして設定されてない属性に値をセットしたときに、例外があがります。スキーマレスな MongoDB ならではの機能です。
include_root_in_json デフォルトは false です。true にした場合は、モデルで #to_json が呼ばれたときに、JSONのトップレベルにドキュメント名が付き、関連のそれぞれのトップレベルにもドキュメント名が付きます。JSON API を作る際に変更が必要になるかも知れません。
parameterize_keys キーの特殊文字SEOフレンドリーな文字へと置換します。デフォルトは true です。
persist_in_safe_mode 全てのデータベース操作をセーフモードで行ないます。MongoDB の採用している GridFS の機能で、データを保存する際、クライアントとサーバでそれぞれMD5ハッシュを生成し、一致しなかった場合は例外を上げます。デフォルトは false です。true にした場合はパフォーマンスが落ちますので気をつけて下さい。
raise_not_found_error id で検索した際に、ドキュメントがなかった場合に、Mongoid::Error::DocumentNotFound 例外を上げます。デフォルトは true です。false にした場合は、例外を上げずに nil を返します。
reconnect_time データベースとつながらない際に、再接続を試みる最大時間を設定します。デフォルトは3秒です。
skip_version_check MongoHQMongoMachine などを使用しているときに、システムのコレクションが接続を許さないせいで、認証の問題が起きた場合に true にして下さい。
ログ

Rails のデフォルトのログ機能を使いたくない場合に、独自のログ機能を設定できます。
config/application.rb

module Hoge
  class Application < Rails::Application
    ...
    config.mongoid.logger = Logger.new($stdout, :warn)
    ...
  end
end
ActiveRecord の削除

これから Mongoid を使用するので、ActiveRecord はロードする必要がありません。
config/application.rb の先頭部分を以下のように書き換えます。

#require 'rails/all'
require "action_controller/railtie"
require "action_mailer/railtie"
require "active_resource/railtie"
他言語

デフォルトで Mongoid は、英語の国際化ファイルが追加されております。他の言語を追加するには、config/initializers/mongoid.rb を作成し以下の内容を追加します。

# adds Spanish
Mongoid.add_language("es")

現在のところ、以下の言語に対応しています。

  • es: スペイン語
  • fr: フランス語
  • it: イタリア語
  • pl: ポーランド語
  • pt: ポルトガル語
  • sv: スウェーデン語

これら全てを有効にしたい場合は、

# include all language that Mongoid knows about
Mongoid.add_language("*")

とします。

Rails 3 以外の設定

Mongoid.configure ブロックを使用して設定することができます。

Mongoid.configure do |config|
  dbname = "hoge_development"
  host = "localhost"
  config.master = Mongo::Connection.new.db(dbname)
  config.slaves = [
    Mongo::Connection.new(host, 27018, :slave_ok => true).db(dbname)
  ]
  config.persist_in_safe_mode = false
end

YAML ファイル、config/mongoid.yml を作成してロードすることもできます。

file_name = File.join(File.dirname(__FILE__), "..", "config", "mongoid.yml")
@settings = YAML.load(ERB.new(File.new(file_name).read).result)

Mongoid.configure do |config|
  config.from_hash(@settings[ENV['RACK_ENV']])
end


インストールについては以上になります。

Rails3 対応 MongoDB ORM、Mongoid 詳解―ドキュメント

インストールに引き続き、ドキュメントを解説します。


ドキュメントは Mongoid のコアオブジェクトであり、データベースに永続化したい全てのオブジェクトは、Mongoid::Document をインクルードしてください。MongoDB でのドキュメントは、BSON オブジェクトとして表現されており、これは Ruby のハッシュや JSON によく似ています。Mongoid のドキュメントは、データベースのコレクションに格納されるか、他のドキュメントにエンベッドされ階層化された形で格納されます。Mongoid ではエンベッドされたオブジェクトもモデルとして表現できるという意味です。

ドキュメント定義

Person を表すシンプルなモデルを考えてみましょう。Person は、first name と last name と middle initial を持ちます。Person オブジェクトにフィールドを設定することにより、これらのアトリビュートを定義することができます。フィールドのデフォルトの型は String になります。

app/models/person.rb

class Person
  include Mongoid::Document
  field :first_name
  field :middle_initial
  field :last_name
end

このモデルは以下のコマンドでも作成できます。

$ rails generate model Person first_name:string middle_initial:string last_name:string


文字列以外の型については明示的に指定しなければなりません。Mongoid で使用できる型は、Array, BigDecimal, Boolean, Date, DateTime, Float, Integer, String, Symbol, Time となります。BigDecimal はデータベース上では String として格納されますが、プログラム側では BigDecimal として扱えます。

app/models/person.rb

class Person
  include Mongoid::Document
  field :birthday, :type => Date
end


フィールドは :default オプションにより、デフォルト値を設定できます。フィールドの型に必ず一致するようにして下さい。また、lambda を使用することも出来ます。

app/models/person.rb

class Person
  include Mongoid::Document
  field :blood_alcohol_level, :type => Float, :default => 0.0
end

ドキュメントのインスタンス

ドキュメントは、new を呼び、インスタンス化します。この時ハッシュを引数に取ります。もし存在しないフィールドがハッシュの属性に含まれていた場合、例外を上げます。また、ハッシュを引数に取る代わりに、ブロックの内部で設定することもできます。

person = Person.new(:first_name => "Ludwig", :last_name => "Beethoven")

person = Person.new do |p|
  p.first_name = "Ludwig"
  p.last_name = "Beethoven"
end

フィールドアクセスのコントロール

attr_protected を使うことで、ハッシュで一括して更新する際に、アトリビュートを保護することができます。逆に、attr_accessible は逆に、設定されてる値をセット可能に、その他を保護することができます。ActiveRecord のものと同様です。これらは、フォームの更新などから、重要なフィールドが思わぬ値をセットされないようにするためにあります。

attr_protected を使った場合、他のすべてのフィールドは、ハッシュで一括して、値をセットできます。

class Person
  include Mongoid::Document
  field :first_name
  attr_protected :_id
end

attr_accessible を使った場合、その他全てのフィールドは、ハッシュで一括しては、値をセットできません。

class Person
  include Mongoid::Document
  field :first_name
  field :last_name
  attr_accessible :first_name, :last_name
end

ダイナミックアトリビュート

Mongoid はデフォルトでは、ダイナミックアトリビュートをサポートします。ダイナミックアトリビュートは、フィールドがドキュメントで設定されてない時でも、値を取得・設定・永続化できるようにします。ただし、ドキュメントクラスのパブリックメソッドなどと重複する名前は使用することができません。


ダイナミックアトリビュートは次の例のように使用できます。

アトリビュートがドキュメントに存在する場合、Mongoid は、通常のゲッター・セッターメソッドを提供します。例えば、Person が gender アトリビュートを含む場合です。

person[:gender] = "Male"
person.gender # => "Male" を返す
person.gender = "Female" #=> gender に "Female" をセット

アトリビュートがドキュメント内に定義されてない場合、Mongoid はゲッター・セッターメソッドを提供せず、通常の method_missing を呼び出します。ダイナミックアトリビュートを使うには、他のアクセサメソッド( = )もしくは( read_attribute と write_attribute )を使わなければなりません。

person[:gender] # => returns nil
person[:gender] = "Male" # => gender に "Male" をセット

person.read_attribute(:age) # => returns nil
person.write_attribute(:age, 35) # => age に 35 をセット

予約された名前

もし、予約された名前とドキュメントのフィールド名が衝突した場合、例外が上がります。予約された名前の一覧は、Mongoid.destructive_fields でみることができます。


ドキュメントについては、以上になります。

Rails3 対応 MongoDB ORM、Mongoid 詳解―関連

Mongoid::Document は、embeds_one, embeds_many, embedded_in といった、ActiveRecord スタイルの3つのマクロを通して、他のドキュメントに対して関連を設定することができます。関連を設定すると、1つのドキュメントが他のすべてのドキュメントのルートになり、全ての関連付けられたオブジェクトはルートドキュメントに埋め込まれます。リレーショナルな関連はこれらのマクロでは設定できません。後述するリレーショナルな関連の項を見てください。


先の例の Person モデルが、他のドキュメントと関連する場合を考えてみましょう。
app/models/person.rb:

class Person
  include Mongoid::Document
  field :first_name
  field :last_name
  embeds_one :address
  embeds_many :phones
end

app/models/address.rb:

class Address
  include Mongoid::Document
  field :street
  field :city
  field :state
  field :post_code
  embedded_in :person, :inverse_of => :address
end

app/models/phone.rb:

class Phone
  include Mongoid::Document
  field :country_code, :type => Integer, :default => 1
  field :number
  embedded_in :person, :inverse_of => :phones
end

上記のモデルが与えられた場合、person は、embeds_one はハッシュ、embeds_many は配列の、BSON 構造としてデータベースに保存されます。embedded_in マクロは、エンベッドがうまく働くように、必ず定義されなければなりません。忘れないで下さい。

{
  first_name: "Durran",
  last_name: "Jordan",
  address: {
    street: "30 Rockefeller Plaza",
    city: "New York",
    state: "NY",
    post_code: "10112"
  },
  phones: [
    { country_code: 1, number: "212-555-1212" },
    { country_code: 1, number: "212-555-1213" }
  ]
}


関連はオプションを取ります。最も大事な必須オプションは、embedded_in マクロにおける inverse_of オプションです。関連を適切に設定し、オブジェクトグラフが、どのオブジェクトに対するどんな変更にも確かに追従することができるように、embedded_in マクロはこのオプションを提供します。その値は親オブジェクトの関連名にしなければなりません。さらに、関連名にクラス名の単数形・複数形と違うものを使いたい場合には、class_name オプションが使用します。前述の例の Person クラスを変更して、phones という関連名を phone_numbers に変更してみましょう。

person.rb:

class Person
  include Mongoid::Document
  field :first_name
  field :last_name
  embeds_one :address
  embeds_many :phone_numbers, :class_name => "Phone"
end

関連のビルドと作成

関連は、以下のように、セット、追加、ビルド、作成することができます。

embeds_one:

person = Person.new

person.build_address(:street => "Oxford Street")
person.create_address(:street => "Oxford Street")
person.address = Address.new(:street => "Oxford Street")

embeds_many:

person = Person.new

person.phone_numbers.build(:number => "415-555-1212")
person.phone_numbers.create(:number => "415-555-1212")
person.phone_numbers << Phone.new(:number => "415-555-1212")
person.phone_numbers = [ Phone.new(:number => "415-555-1212") ]

embedded_in:

address = Address.new
address.person = Person.new(:first_name => "Mark")

ポリモーフィックな関連

デフォルトでは、embedded_in 関連は既にポリモーフィックです。どんな名前を与えようと、常に親オブジェクトを返します。「安心毛布」として、:polymorphic => true オプションを付けられますが、実際には何も行ないません。

address.rb:

class Address
  include Mongoid::Document
  field :street
  field :city
  field :state
  field :post_code
  embedded_in :addressable, :inverse_of => :address
end

この例は、address.addressable が実際には Person である親オブジェクトを返します。

関連の拡張

Mongoid は無名の関連の拡張をサポートします。@target インスタンス変数を使用して、プロクシされたターゲットにアクセスできます。

person.rb:

class Person
  include Mongoid::Document
  field :name
  embeds_many :addresses do
    def california
      @target.select { |address| address.state == "CA" }
    end
  end
end

上記の例では、person.addresses.california はカルフォルニアのアドレスのみを返します。

リレーショナルな関連

Mongoid は、他のコレクションのドキュメントや他のデータベースにあるオブジェクトへの、基本的なリレーショナルな関連をサポートします。関連付けられたオブジェクトは、関連がうまく動くように、ActiveRecord スタイルのファインダーをサポートしていなければなりません。リレーショナルな関連では、references_one, references_many, referenced_in の3つのマクロが提供されます。マクロを使用したとき、foo_id フィールドが referenced_in 側に作成されます。stored_as オプションを使ったときは、逆に bar_ids が配列として保存されます。

class Person
  include Mongoid::Document
  references_one :policy
  references_many :prescriptions
  references_many :preferences, :stored_as => :array, :inverse_of => :people
end

class Policy
  include Mongoid::Document
  referenced_in :person
end

class Prescription
  include Mongoid::Document
  referenced_in :person
end

class Preference
  include Mongoid::Document
  references_many :people, :stored_as => :array, :inverse_of => :preferences
end

person = Person.create
policy = Policy.create
prescription = Prescription.create

person.policy = policy
person.prescriptions = [prescription]

(筆者注:Mongoid 2.0.0.beta.16 では以下のようにしないと保存できませんでした。この辺はリファクタリングしている最中っぽいです。バージョンアップ時にこの記事も更新します。)

person.save #=> ダメ。
policy.save
prescription.save

p1 = Preference.create
p2 = Preference.create
person.preference_ids = [p1._ids, p2._ids]
p1.person_ids = [person._id]
p2.person_ids = [person._id]
person.save
p1.save
p2.save

カスケード削除

ActiveRecord と同じように、親オブジェクトが削除された場合に、子オブジェクトも削除したい場合は、references_one と references_many マクロに :dependent オプションを付けます。

class Person
  include Mongoid::Document
  references_one :policy, :dependent => :destroy
  references_many :prescriptions, :dependent => :delete
end


関連については以上です。