Rails3 対応 MongoDB ORM、Mongoid 詳解―エクステンション

FABRICATION

Paul Eliott さんの Fabrication Gem は、オブジェクト生成ライブラリです。Mongoid を最初からサポートしており、テストの簡便のために、オブジェクトを生成する素敵な構文を提供しています。

Fabricator(:person) do
  title "Grand Poobah"
  addresses(:count => 2) do |address, i|
    Fabricate(:address, :streeet => "#{i} Bond St.")
  end
end

MONGOID-RSPEC

Evan Sagge さんの mongoid-rspec は、Mongoid 用の RSpec のマッチャーを提供します。マッチャーには、関連、オプション、バリデーション、フィールドが含まれます。

describe Person do
  it { should reference_one :account }
  it { should reference_many :posts }
  it { should be_referenced_in :organization }

  it { should validate_presence_of(:name) }

  it { should have_field(:age).of_type(Integer) }
end

describe Address do
  it { should be_embedded_in(:person).as_inverse_of(:addresses) }
end

REMARKABLE

Brian Cardarella さんの remarkable-mongoid Gem は、Mongoid 用の RSpec マッチャーの素敵な代替を提供します。マッチャーは以下の例の他に、Remarkable::ActiveModel を元に、全てのバリデーションを含みます。

describe Person do

  it { should reference_one :account }
  it { should reference_many :posts }
  it { should be_referenced_in :organization }
  it { should embed_one :name }
  it { should embed_many :addresses }
  it { should be_embedded_in :group }

  it { should validate_uniqueness_of :dob }
end

RIOT

Riot-Mongoid Gem は、Mongoid 用の riot アサーションを提供します。フィールド、キー、関連、バリデーションのアサーションが含まれます。

context "Person Model" do
  setup { Person.new }

  asserts_topic.has_field :title,       :type => String

  asserts_topic.has_association :references_one, :account
  asserts_topic.has_association :embeds_many, :addresses

  asserts_topic.has_validation :validates_presence_of, :title
end


エクステンションは以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―インテグレーション

CARRIERWAVE

ファイルアップロードを扱う Carrierwave は Mongoid をサポートしています。現在のところ、ファイルと S3 に格納することを指定できます。:mount_on オプションがアップローダークラスに定義されてない場合は、ファイル名が アップローダー名_filename をフィールド名として、格納されます。

class User
  include Mongoid::Document
  mount_uploader :avatar, AvatarUploader #field is avatar_filename
end

CUCUMBER

MongoDB はトランザクションをサポートしていないので、それぞれのフィーチャーが実行される前に、データベースがクリーンになるように、Cucumber にフックを追加したいと思います。

features/support/hooks.rb:

Mongoid.master.collections.select do |collection|
  collection.name !~ /system/
end.each(&:drop)


もし、database_cleaner という Gem を使っているなら、代わりに、features/support/database_cleaner.rb を作成してください。

require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.orm = "mongoid"
Before { DatabaseCleaner.clean }

RSPEC

Cucumber と同じように、RSpec の use_transactional_fixtures は Mongoid では影響しません。スイートを実行した後、データベースをクリーンにすることができます。

spec/spec_helper.rb:

Rspec.configure do |config|
  config.after :suite do
    Mongoid.master.collections.select do |collection|
      collection.name !~ /system/
    end.each(&:drop)
  end
end

おまけに :each の後何かすることもできますが、たくさんのインテグレーションスペックがある場合、遅くなるので注意してください。


もし、database_cleaner という Gem を使っているなら、代わりに、spec/spec_helper.rb の RSpec コンフィグブロックで以下の行を付け足してください。

Rspec.configure do |config|
  require 'database_cleaner'
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.orm = "mongoid"
  end

  config.before(:each) do
    DatabaseCleaner.clean
  end
end

DEVISE

Mongoid で動くように Devise をセットアップするためにやることは、ActiveRecord を呼び出しているところを削除して、Mongoid に置き換えることです。

config/initializers/devise.rb:

# ==> ORM configuration
# Load and configure the ORM. Supports :active_record (default),
# :mongoid (bson_ext recommended) and :data_mapper (experimental).
require "devise/orm/mongoid"

PASSENGER

MongoDB ウィキでは、Passenger の Smart spawning が有効なとき(デフォルトでは Conservative spawning です)、イニシャライザでインクルードする必要があると警告してますが、Mongoid では、特に何かする必要はありません。Passenger がこのモードで起動しているとき、ワーカーがフォークされたら、Mongoid はそれを検知し、再接続するようになっています。

UNICORN

Unicorn では、preload_app を true にセットする以外は、特に Mongoid で必要な設定はありません。Unicorn が子ワーカーをフォークしたら、Mongoid はそれを検知し、次のクエリでデータベースへ再接続します。


インテグレーションは以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―Rakeタスク

Mongoid は Rails 3 環境で以下の Rake タスクを提供します。

db:create 依存関係のために存在します。実際には何もしません。
db:create_indexes モデルから全てのインデックス定義を読み取り、データベースにそれらを作成します。
db:drop システム用コレクションを除いて、全てのコレクションをデータベースから削除します。
db:migrate 依存関係のために存在します。実際には何もしません。
db:schema:load 依存関係のために存在します。実際には何もしません。
db:seed db/seeds.rb からデータベースに初期データを作成します。
db:setup インデックスを作成し、データベースに初期データを作成します。
db:test:prepare 依存関係のために存在します。実際には何もしません。


Rake タスクは以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―その他

Mongoid にはアプリケーションで使える、いくつかの役に立つ機能があります。

マスター/スレイブ サポート

mongoid.yml でスレイブデータベースを設定しているなら、スレイブを有効化した読み取りクエリを、ラウンドロビンでスレイブデータベースを参照します。また、クエリ単位でも制御することができます。


モデルの全ての読み取りを、スレイブ間でラウンドロビンします:

class Person
  include Mongoid::Document
  enslave
end


enslave Criteria を使って、クエリ単位で、スレイブから読み取ります:

Person.where(:first_name => "Durran").enslave

キャッシュ

Mongoid は、何も設定していないそのままの状態で、大きなクエリとデータセットでメモリが効率的に使われるように、MongoDB の Ruby ドライバーのカーソルをラップしています。しかしながら、メモリの中に全てのヒットしたドキュメントをロードしたい場合や、それらをデータベースに複数回問い合わせることなく返したい場合に、cashe マクロと Criteria を使うことができます。


モデルに対する全てのクエリがキャッシュされます:

class Person
  include Mongoid::Document
  cache
end


クエリ単位でのキャッシュ:

Person.where(:first_name => "Durran").cache

ダーティー・アトリビュート

Mongoid には、ActiveModel スタイルのダーティー・アトリビュート・モジュールが実装されています。これらは、ActiveModel の API に厳密に従います:

class Person
  include Mongoid::Document
  field :first_name
end

person = Person.new(:first_name => "Leroy")
person.first_name = "Lauren"
person.changed? # true
person.first_name_changed? # true
person.first_name_was # Leroy
person.first_name_change # [ "Leroy", "Lauren" ]
person.changes # { "first_name" => [ "Leroy", "Lauren" ] }

パラノイド ドキュメント

ドキュメントを削除するとき、削除フラグをつけて、実際にはデータベースから削除したくない場合があると思います。Mongoid はそういう時のために Pranoia モジュールを提供しています。

class Person
  include Mongoid::Document
  include Mongoid::Paranoia
end

person.delete   # deleted_at フィールドに現在時刻をセットします
person.delete!  # ドキュメントを完全に削除します
person.destroy! # コールバックとともにドキュメントを完全に削除します
person.restore  # 削除されたドキュメントを復帰します

バージョン管理

Mongoid は、Mongoid::Versioning モジュールをインクルードすることにより、シンプルなバージョン管理をサポートします。このモジュールをインクルードすれば、保存の時に、version フィールドを作成します。version 番号は整数で、保存のたびに更新されます。

class Person
  include Mongoid::Document
  include Mongoid::Versioning
end


max_versions を設定することができます。Mongoid は最新のバージョンが設定された最大値を超えないようにします。

class Person
  include Mongoid::Document
  include Mongoid::Versioning

  # 最大5バージョンだけ記録する
  max_versions 5

end

タイムスタンプ

Mongoid は Mongoid::Timestamps モジュールで、タイムスタンプをサポートします。created_at と updated_at フィールドが、それぞれ自動的に付与されます。

class Person
  include Mongoid::Document
  include Mongoid::Timestamps
end

キー

key マクロを使うことで、デフォルトの id の代わりに、複合キーを作成することができます。

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

person = Person.new(:first_name => "Syd", :last_name => "Vicious")
person.id # "syd-vicious" を返します。

違うコレクションに保存する

store_in マクロを使えば簡単です。

class Person
  include Mongoid::Document
  store_in :students
end


その他は以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―インデックス

index マクロを使うことにより、ドキュメントにインデックスを定義することができます。:unique オプションをつけると、ユニークなインデックスを構築できます。オプションは必須ではありません。

class Person
  include Mongoid::Document
  field :ssn
  index :ssn, :unique => true
end


以下のようにして、エンベッドされたドキュメントにもインデックスを定義することができます。

class Person
  include Mongoid::Document
  embeds_many :addresses
  index "addresses.street"
end


複数フィールドに対してもインデックスを定義することができます。また、ソート順も定義できます。

  include Mongoid::Document
  field :first_name
  field :last_name
  index(
    [
      [ :first_name, Mongo::ASCENDING ],
      [ :last_name, Mongo::ASCENDING ]
    ],
    :unique => true
  )
end


インデックスは、ある程度時間がかかる場合、バックグラウンドで実行することもできます:

class Person
  include Mongoid::Document
  field :ssn
  index :ssn, :unique => true, :background => true
end


位置空間インデックスを定義する場合は、必ず、フィールドを配列としてください。

class Person
  include Mongoid::Document
  field :location, :type => Array
  index [[ :location, Mongo::GEO2D ]], :min => 200, :max => 200
end


リレーショナルな関連で、外部キーに対して、インデックスを定義することができます。

class Comment
  include Mongoid::Document
  referenced_in :post, :inverse_of => :comments, :index => true
  references_many \
    :users,
    :stored_as => :array,
    :inverse_of => :comments,
    :index => true
end


データベースにインデックスを作成したい時は、Rake タスクを呼び出してください:

rake db:create_indexes


モデルクラスがロードされたときに、自動的にインデックスを作成したい場合は、mongoid.yml のコンフィギュレーションオプションで設定することができます。ただし、プロダクション環境では推奨しません

defaults: &defaults
  autocreate_indexes: true


インデックスは以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―継承

Mongoid はドキュメントとエンベッドされたドキュメントの継承をサポートしています。以下のドメインモデルを考えてみます。

class Canvas
  include Mongoid::Document
  field :name
  embeds_many :shapes

  def render
    shapes.each { |shape| shape.render }
  end
end

class Browser < Canvas
  field :version, :type => Integer
end

class Firefox < Browser
  field :useragent
end

class Shape
  include Mongoid::Document
  field :x, :type => Integer
  field :y, :type => Integer

  embedded_in :canvas, :inverse_of => :shapes

  def render
    #...
  end
end

class Circle < Shape
  field :radius, :type => Float
end

class Rectangle < Shape
  field :width, :type => Float
  field :height, :type => Float
end

上記の例で、Canvas、Browser、Firefox は全て canvases コレクションの中に保存されます。データベースから正しいドキュメントが帰ってくるように、追加のアトリビュート _type が保存されます。また、これは、Circle、Rectangle、Shape といった、エンベッドされたドキュメントにも適用されます。フィールドとバリデーションも子に継承されますが、親には適用されません。子クラスは親クラスの全てのフィールドとバリデーションを含みますが、逆はありません。

子クラスへのクエリ

子クラスへのクエリは通常のクエリと変わりません。ドキュメントはすべて同じコレクションですが、クエリは、正しい型のドキュメントのみ返します。

Canvas.where(:name => "Paper") # Canvas ドキュメントと子クラスを返します
Firefox.where(:name => "Window 1") # Firefox ドキュメントだけ返します

関連

通常の代入を通してか、関連の build や create メソッドを通して、has_one または has_many 関連の、どの型の子クラスも追加できます:

firefox = Firefox.new
firefox.shapes.build({ :x => 0, :y => 0 }) # Shape オブジェクトを作成します
firefox.shapes.build({ :x => 0, :y => 0 }, Circle) # Circle オブジェクトを作成します
firefox.shapes.create({ :x => 0, :y => 0 }, Rectangle) # Rectangle オブジェクトを作成します

rect = Rectangle.new(:width => 100, :height => 200)
firefox.shapes


継承は以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―バリデーション

Mongoid は、基本的なバリデーションを提供するために ActiveModel::Validations を含んでおり、さらに関連と一意性バリデーションに手を加えています。

さらに詳しい情報は、ActiveModel::Validations のドキュメントを参照してください。


全てのバリデーションには以下の共通のオプションがあります。

  • :allow_nil アトリビュートが nil を許すかどうかを指定します。
  • :if 与えられた値が true と評価された場合だけ実行されます。
  • :on 指定された :create や :update の時だけ実行されます。
  • :unless 与えられた値が false と評価された場合だけ実行されます。

ブール値のフィールドが受け入れ可能(デフォルト: true)かどうかを検証します:

validates_acceptance_of :terms

オプション:

  • :accept 受け入れる値を指定します。
  • :message カスタムエラーメッセージを定義します。

関連付けられているドキュメントも一緒に検証します:

validates_associated :addresses, :employers

オプション:

  • :message カスタムエラーメッセージを定義します。

末尾に "_confirmation" がついたアクセサと一致するかを検証します:

validates_confirmation_of :password

オプション:

  • :message カスタムエラーメッセージを定義します。

与えられた値が含まれないかを検証します:

validates_exclusion_of :employers, :in => ["Hashrocket"]

オプション:

  • :in 含まれない配列か範囲。
  • :message カスタムエラーメッセージを定義します。

フィールドのフォーマットを検証します:

validates_format_of :title, :with => /[A-Za-z]/

オプション:

  • :in 含まれる配列か範囲。
  • :allow_blank 空の値を許すかどうか指定します。
  • :with マッチする正規表現
  • :without マッチしない正規表現
  • :message カスタムエラーメッセージを定義します。

与えられた値が含まれるかを検証します。

validates_inclusion_of :employers, :in => ["Hashrocket"]

オプション:

  • :allow_blank 空の値を許すかどうか指定します。
  • :in 含まれる配列か範囲。
  • :message カスタムエラーメッセージを定義します。

フィールドの長さを検証します:

validates_length_of :password, :minimum => 8, :maximum => 16

オプション:

  • :allow_blank 空の値を許すかどうか指定します。
  • :in 長さの範囲を指定します。
  • :within 長さの範囲を指定します。
  • :maximum アトリビュートの最大の長さを指定します。
  • :minimum アトリビュートの最小の長さを指定します。
  • :tokenizer ブロックで文字のカウント方法を定義します。
  • :message カスタムエラーメッセージを定義します。
  • :too_long 長すぎた時のカスタムエラーメッセージを定義します。
  • :too_short 短すぎた時のカスタムエラーメッセージを定義します。
  • :wrong_length 間違った長さの時のカスタムエラーメッセージを定義します。

フィールドの数値を検証します:

validates_numericality_of :age, :even => true

オプション:

  • :equal_to フィールドが指定した値と一致するか検証します。Specify a value the field must be exactly.
  • :greater_than フィールドが指定した値より大きいか検証します。
  • :greater_than_or_equal_to フィールドが指定した値より大きいか等しいかを検証します。
  • :less_than フィールドが指定した値より小さいか検証します。
  • :less_than_or_equal_to フィールドが指定した値より小さいか等しいかを検証します。
  • :even 偶数かどうかを指定します。
  • :odd 奇数かどうかを指定します。
  • :only_integer 整数かどうかを指定します。
  • :message カスタムエラーメッセージを定義します。

フィールドが存在するかを懸賞します:

validates_presence_of :first_name

オプション:

  • :message カスタムエラーメッセージを定義します。

フィールドがデータベースで一意かどうかを検証します:

※注意:エンベッドされたドキュメントに使用した場合、親ドキュメントのコンテキストの範囲内で一意かどうかをチェックします。データベース全体ではありません。

validates_uniqueness_of :ssn

オプション:

  • :message カスタムエラーメッセージを定義します。


バリデーションは以上です。