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 カスタムエラーメッセージを定義します。


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

Rails3 対応 MongoDB ORM、Mongoid 詳解―コールバック

Mongoid は以下のコールバックをサポートしています:

  • before_create
  • before_destroy
  • before_save
  • before_update
  • before_validation
  • after_create
  • after_initialize
  • after_destroy
  • after_save
  • after_update
  • after_validation

コールバックは、他のドキュメントにエンベッドされたものも含めて、全てのドキュメントにあります。Mongoid は永続化アクションが実行された時だけ、コールバックを発動します。Mongoid の狙いは、広い階層をサポートすること、ドキュメントのすべての階層にまたがって実行されない最適化されたアトミックな更新を扱うことにあります。

article.rb:

class Article
  include Mongoid::Document
  field :name
  field :body
  field :slug

  before_create :generate_slug

  protected
  def generate_slug
    self.slug = name.to_permalink
  end
end


コールバックは ActiveModel 由来なので、以下のような新しい構文を使うことができます:

article.rb:

class Article
  include Mongoid::Document
  field :name
  field :body
  field :slug

  set_callback(:save, :before) do |document|
    document.generate_slug
  end

  protected
  def generate_slug
    self.slug = name.to_permalink
  end
end

Rails3 対応 MongoDB ORM、Mongoid 詳解―クエリ

Mongoid は、データベースのドキュメントの問い合わせに、2種類のスタイルをサポートします。最初のスタイルは、ActiveRecord風の、ファインダー/動的なファインダーの構文です。2つ目のスタイルは、Mongoid の Criteria API を使った推奨メソッドです。(訳注: 2つ目は Rails 3 の Arel 風です。)

ファインダー

ファインダーはドキュメントのクラスメソッドです。データベースのオブジェクトを探索するのに、条件のハッシュか文字列を受け付けます。Document.all、Document.count、Document.find、Document.first、Document.last、Document.paginate があります。これらに加えて、アトリビュートが一致しない場合に、作成・初期化する、ファインダー兼作成構文があります。


与えられた条件に合う全てのドキュメントを検索します:

Person.all(:conditions => { :first_name => "Syd" })
Person.find(:all, :conditions => { :first_name => "Syd" })


与えられた条件に合う最初のドキュメントを検索します:

Person.first(:conditions => { :first_name => "Syd" })
Person.find(:first, :conditions => { :first_name => "Syd" })


与えられた条件に合う最初のドキュメントを検索します。ソートパラメーターが与えられないときは、id の逆順でソートされ、取り出されます:

Person.last(:conditions => { :first_name => "Syd" })
Person.find(:last, :conditions => { :first_name => "Syd" })


動的なファインダーをつかって検索します。オブジェクトが存在しない場合、作成/初期化されます:

Person.find_or_create_by(:first_name => "Syd")
Person.find_or_initialize_by(:first_name => "Syd")

Criteria API

データベース問い合わせの、より推奨するメソッドは、Criteria API を使ったものです。Criteria API は、皆さんがよく馴染んだ SQL のように、とても快適な DSL です。Criteria 問い合わせは、データベースからはレイジーロード(遅延ロード)されるので、無限にチェインすることができます。


新しい Criteria を生成するのにいくつかの違った方法があります:

all_people = Mongoid::Criteria.new(Person)
all_people_again = Person.criteria
all_people_names_only = Person.only(:first_name, :last_name)
people_over_18 = Person.where(:age.gt => 18)

上記の例の、最初の2つは Person に対する空の Criteria を生成します。3番目は自動的にフィールド選択を付加し、4番目は既に where セレクタがついたものを生成します。Mongoid::Finders に定義されたどのエントリポイントメソッドを使ってもかまいません。それらはドキュメントのクラスメソッドとして定義されています。下記がその一覧です。

Criteria メソッド

さまさまなタイプの条件を、チェインして追加できます。


Criteria#all_in: 与えられた全ての値が一致した時にマッチします。配列で厳密に一致させるときなどに有用です。

Person.all_in(:aliases => [ "Jeffrey", "The Dude" ])


Criteria#any_in: 配列のいずれかの値にフィールドが一致したドキュメントにマッチします。

Person.any_in(:status => ["Single", "Divorced", "Separated"])


Criteria#any_of: 与えられたいずれかの条件に合うドキュメントにマッチします。これは、MongoDB では、$or クエリです。複数のフィールドや同じフィールドに対する複数の条件を含むことができます。

Person.any_of({ :status => "Single" }, { :preference => "Open" })


Criteria#and: それぞれのフィールドと値のペアが一致するドキュメントにマッチします。where のエイリアスで、良いシンタックスシュガーです。

Person.and(:age.gt => 18, :gender => "Male")


Criteria#count: これは、他の Criteria の後にチェインされなければなりません。AcitiveRecord スタイルの count の使用を妨げるものではありません。

Person.where(:status => "Married").count


Criteria#excludes: キーと値のペアが一致しないドキュメントにマッチします。

Person.excludes(:status => "Married")


Criteria#id: 与えられたidと一致するドキュメントにマッチします。

Person.criteria.id("4b2fe28ee2dc9b5f7b000029")


Criteria#limit: 結果をある数に制限します。

Person.limit(20)


Criteria#near: 指定した点から一番近いものにマッチする、位置空間の問い合わせです。(訳注:MongoDB の位置空間インデックスのページを参考にしてください)

Address.near(:position => [ 37.7, -122.4, 10 ])


Criteria#not_in: 与えられたリストのいずれにも一致しないドキュメントにマッチします。

Person.not_in(:status => ["Divorced", "Single"])


Criteria#only: データベースから返るフィールドを制限します。最適化に有用です。

Person.only(:first_name, :last_name)


Criteria#order_by: ソート条件を加えます。(注意:ソートするフィールドはインデックス化しておいてください)

# asc/ascending と desc/descending 条件をつなげることができます。
Person.desc(:last_name).asc(:first_name)
Person.descending(:last_name).ascending(:first_name)

# シンボルのリストも通ります
Person.order_by(:last_name.desc, :first_name.asc, :city.desc)

# 配列の配列でも通ります
Person.order_by([[:last_name, :desc], [:first_name, :asc]])


Criteria#skip: ある数だけドキュメントをスキップします。SQL の OFFSET と似たようなものです。

Person.skip(100)


Criteria#where: それぞれのフィールドと値のペアが一致したドキュメントにマッチします。

Person.where(:age.gt => 18, :gender => "Male")

正規表現

文字列に対して正規表現でマッチさせることができます。

Person.where(:last_name => /^Jord/)

同じフィールドに対する複数の式

Mongoid は複数の式を1つの式に結合することができます。

# { "age" => { "$gt" => 18, "$lt" => 30 } } セレクタを生成します
Person.where(:age.gt => 18, :age.lt => 30)

全てを一緒に使う

post_code が 94133 である Person の first_name と last_name フィールドだけ返す:

Person.only(:first_name, :last_name).where("address.post_code" => "94133")


last_name が "Vicious" で、contry_code が 1 の人々の first_name だけ返す。

Person.only(:first_name).where("phones.country_code" => 1).in(:last_name => ["Vicious"])


last_name が "Zorg" で、middle_initial が "J" の、全ての人々の全てのフィールドを返す

Person.where(:last_name => "Zorg").and(:middle_initial => "J")


MongoDBの式を使った、where 条件の例(訳注:「ハンズオンで分かる MongoDB チュートリアル」などを参考にしてください):

Person.where(:title.all => ["Sir"])
Person.where(:age.exists => true)
Person.where(:age.gt => 18)
Person.where(:age.gte => 18)
Person.where(:title.in => ["Sir", "Madam"])
Person.where(:age.lt => 55)
Person.where(:age.lte => 55)
Person.where(:title.ne => "Mr")
Person.where(:title.nin => ["Esquire"])
Person.where(:aliases.size => 2)
Person.where(:location.near => [ 22.5, -21.33 ])
Person.where(:location.within => { "$center" => [ [ 50, -40 ], 1 ] })

Criteria のチェイン

Criteria は、DataMapper のように、ドキュメントのクラスメソッドを使ってチェインできます。下記の例は、60歳以上の男性を返します。

person.rb:

class Person
  include Mongoid::Document

  field :gender
  field :age

  class << self
    def men
      criteria.where(:gender => "Male")
    end
    def old
      criteria.where(:age => { "$gt" => 60 })
    end
  end
end

Person.old.men # 新しい2つの Criteria を合成して返します

演算、グルーピング、集約

Mongoid はいくつかの基本的な演算、グルーピング、集約操作を提供します。


データベースからあるフィールドの最大・最小値を検索します。結果は Float で返ります:

Person.max(:age)
Person.min(:age)


全てのドキュメントをまたがって、あるフィールドの合計を返します。結果は Float で返ります:

Invoice.sum(:total)


全てのドキュメントから与えられたフィールドのカウントを集約します。結果はハッシュの配列が返ります。

Person.only(:first_name).where(:age.gt => 18).aggregate #=> [{"first_name"=>"Dave", "count"=>4.0}, {"first_name"=>"Durran", "count"=>2.0}], ...


与えられたフィールドに対してドキュメントのグループを取得します。結果はハッシュの配列が返ります。

Person.only(:first_name).where(:age.gt => 18).group #=>  [{"first_name"=>"Durran", "group"=>[#<Person _id: 4c5f404bd0d8191780000001, preference_ids: nil, last_name: "Jordan", first_name: "Durran">]}, {"first_name"=>"Ian", "group"=>[#<Person _id: 4c64bb6fd0d8191921000001, preference_ids: [], last_name: "Obama", first_name: "Ian">]}, ...]

スコープ

Mongoid は ActiveRecord 3.0 のスコープにとてもよく似たスコープを提供します。スコープは、ハッシュ、ハッシュの proc、Criteria、Criteria の proc で表されます。また、拡張したい場合は、ブロックを追加することができます。スコープは、ルートドキュメントかエンベッドされた has_many 関連に、追加できます。スコープは、関連の小クラスからは直接呼び出せません。親クラスのインスタンスから呼び出さなければなりません。スコープはまた、永続化された関連を必要としません。例を以下に挙げます。


ネームドスコープはお互いにチェインできます。また、Criteria を返すクラスメソッドともチェインできます。下記の例は全てのケースを説明しています。

class Player
  include Mongoid::Document
  field :active, :type => Boolean
  field :frags, :type => Integer
  field :deaths, :type => Integer
  field :status

  embeds_many :games

  scope :active, where(:active => true) do
    def count
      size
    end
  end
  scope :inactive, :where => { :active => false }
  scope :frags_over, lambda { |count| { :where => { :frags.gt => count } } }
  scope :deaths_under, lambda { |count| where(:deaths.lt => count) }

  class << self
    def alive
      where(:status => "Alive")
    end
  end
end

class Game
  include Mongoid::Document
  field :saved, :type => Boolean, :default => false
  field :level, :type => Integer, :default => 1
  field :studio
  embedded_in :player, :inverse_of => :games

  scope :saved, where(:saved => true)

  class << self
    def blizzard
      where(:studio => "Blizzard")
    end
  end
end

Player.active # アクティブプレーヤーを返します
Player.active.count # アクティブプレーヤーのカウントを返します
Player.active.alive # アライブなアクティブプレーヤーを返します
Player.inactive.frags_over(10) # フラッグが10以上のインアクティブプレーヤーを返します
Player.deaths_under(30) # 30回死んだプレーヤーを返します

player = Player.find(id)
player.games.saved # ゲームをセーブしたプレーヤーを返します
player.games.saved.blizzard # ブリザード社のゲームをセーブしたプレーヤーを返します


クエリについては以上です。

Rails3 対応 MongoDB ORM、Mongoid 詳解―永続化

今回は、データベースへの Insert、Update、Delete です。


Mongoid は ActiveRecord スタイルのデータベースへの永続化メソッドをサポートしています。永続化戦略の項は、実際に実行されるデータベースクエリについて、注意して見てください。

作成

新しいドキュメントを作成し、データベースに格納するには、Document.create や Document.create! を使います。後者は、バリデーションが失敗したときに例外を上げます。

person = Person.create(:first_name => "Syd", :last_name => "Vicious")
person = Person.create!(:first_name => "Emmanuel", :last_name => "Zorg")

保存

ドキュメントをデータベースに格納するには、Document.save または Document.save! を使います。後者は、バリデーションが失敗したときに例外を上げます。

person = Person.new(:first_name => "Syd", :last_name => "Vicious")
person.save # or person.upsert

person = Person.new(:first_name => "Emmanuel", :last_name => "Zorg")
person.save!

アトリビュートの更新

新しいアトリビュートをドキュメントに書き込み、保存するには、Document.update_attrributes と Document.update_attributes! を使います。後者は、バリデーションが失敗したときに例外を上げます。

person = Person.new(:first_name => "Syd", :last_name => "Vicious")
person.update_attributes(:first_name => "Nancy")

person = Person.new(:first_name => "Emmanuel", :last_name => "Zorg")
person.update_attributes!(:first_name => "Nancy")

削除

データベースからドキュメントを削除するには、Document#destroy、 Document#delete、Document.destroy_all、Document.delete_all を使います。最初の2つは、1つのドキュメントを削除し、後の2つは、与えた条件に合う全てのドキュメントを削除します。これらの削除メソッドは、独自に定義した全てのコールバックを無視するのに気をつけてください。これらのメソッドはまた、関連から呼ぶこともできます。

person = Person.create(:first_name => "Syd", :last_name => "Vicious")
person.destroy

person = Person.create(:first_name => "Syd", :last_name => "Vicious")
person.delete

Person.destroy_all(:conditions => { :first_name => "Syd", :last_name => "Vicious" })
Person.delete_all(:conditions => { :first_name => "Syd", :last_name => "Vicious" })

person.addresses.delete_all
person.addresses.destroy_all(:conditions => { :street => "Bond St" })

セーフモードでの永続化

Mongoid は、全体設定のオプションで、セーフモードでの永続化を提供していますが、もし、デフォルトの false にしている場合でも、永続化メソッドの前に safely を追加するだけで、1クエリ毎にセーフモードを使うことができます。

Person.safely.create(:title => "King")
Person.safely.delete_all

person.safely.save
person.safely.destroy

修飾子

Mongoid は $inc 修飾子をサポートしています。

person = Person.find(id)
person.inc(:score, 100)
person.safely.inc(:score, 100) # Update in safe mode.

永続化戦略と裏で走るデータベースクエリ

これからの例では、以下のモデルに基づいて、説明します。(裏で走るデータベースクエリの読み方については、拙作「ハンズオンで分かる MongoDB チュートリアル」を参考にしてください。)

class Person
  include Mongoid::Document
  field :first_name
  field :middle_initial
  field :last_name
  embeds_one :email
  embeds_many :addresses
end

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

class Email
  include Mongoid::Document
  field :address
  embedded_in :person, :inverse_of => :email
end

新しいドキュメントの作成

シナリオ:新しいルートドキュメントを保存する。

ドキュメントを、全てのフィールドとともに、コレクションに挿入します。

Mongoid では:

person = Person.new(:first_name => "Dudley")
person.save

MongoDB のクエリでは:

db.people.insert({ "first_name" : "Dudley" }, true);
シナリオ:新しいルートドキュメントを、子供とともに保存する。

ドキュメントを、全てのフィールドと、子要素とともに、コレクションに挿入します。save はどちらから呼んでも問題ありません。

Mongoid では:

person = Person.new(:first_name => "Dudley")
address = Address.new(:street => "Upper Street")
person.addresses << address
person.save # address.save でも同様の結果になる

MongoDB のクエリでは:

db.people.insert(
  { "first_name" : "Dudley", "addresses" : [ { "street" : "Upper Street" } ] },
  true
);
シナリオ:既に存在するルートドキュメントの中に、新しい embeds_one を保存する。

ルートドキュメントの中へ、新しいエンベッドされたアトリビュートを、アトミックに更新します。これは子要素から呼ばれないといけません。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
email = Email.new(:address => "dudley@moore.com")
person.email = email
email.save

MongoDB のクエリでは :(person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$set" : { "email" : { "address" : "dudley@moore.com" } } },
  false,
  true
);
シナリオ:既に存在するルートドキュメントの中に、新しい embeds_many を保存する。

ルートドキュメントの中の配列へ、新しいエンベッドされたアトリビュートを、アトミックに追加します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
address = Address.new(:street => "Upper Street")
person.addresses << address
address.save

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$push" : { "addresses" : { "street" : "Upper Street" } } },
  false,
  true
);

既にあるドキュメントを更新する

既存のドキュメントを更新するとき、Mongoid のダーティーアトリビュートモジュールは、変更のあったものだけ更新するようになっています。save メソッドは、更新されたドキュメントから呼ばなければなりません。(これは今後のリリースで変更される予定です。ドキュメントツリー全体を確認してください。)

シナリオ:ルートドキュメントを更新する。

ダーティーアトリビュートを、アトミックに更新します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
person.last_name = "Moore"
person.save

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$set" : { "last_name" : "Moore" } },
  false,
  true
);
シナリオ:既にある embeds_one を更新する。

既にエンベッドされているドキュメントを、アトミックに更新します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
email = person.email
email.address = "dudley@moore.org"
email.save

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$set" : { "email.address" : "dudley@moore.org" } },
  false,
  true
);
シナリオ:既にある embeds_many を更新する。

既にエンベッドされているドキュメントを、配列のその部分だけ、アトミックに更新します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
address = person.addresses.first
address.street = "Clerkenwell Road"
address.save

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$set" : { "addresses.0.street" : "Clerkenwell Road" } },
  false,
  true
);

混合した永続化

Mongoid の永続化では、永続化コマンドが実行され、全ての更新は、その下の階層のエンベッドされたものの更新も同様に、アトミックです。このため、ドキュメントツリー全体を扱うのに便利で、1回のデータベース呼び出しで済みます。ベータ8リリースでは、削除はこういう風に動きませんので、別々に呼び出してください。

シナリオ:ルートドキュメントと、子要素を、新しい子要素を追加した上で、更新する。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
person.last_name = "Moore"
person.email.address = "d.moore@gmail.com"
person.addresses.build(:street => "Upper Street")
person.save

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  {
    "$set" :
    { "last_name" : "Moore", "email.address" : "d.moore@gmail.com" },
    "$push" :
    { "addresses" : { "street" : "Upper Street" } }
  },
  false,
  true
);

ドキュメントの削除

シナリオ:既にあるルートドキュメントを削除する。

ドキュメントの id でコレクションから削除します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
person.delete # destroy メソッドでも動きます

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.remove({ "_id" : "4baa56f1230048567300485c" }, true);
シナリオ:既にある embeds_one を削除する。

エンベッドされたドキュメントをアトミックに更新します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
email = person.email
email.delete # もしくは destroy

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c" だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$unset" : { "email" : true } },
  false,
  true
);
シナリオ:既にある embeds_many を削除する。

エンベッドされた配列のドキュメントをアトミックに更新します。

Mongoid では:

person = Person.where(:first_name => "Dudley").first
address = person.addresses.first
address.delete # or destroy
The MongoDB query: 
(Assume person.id = "4baa56f1230048567300485c") 
(Assume address.id = "4baa56f1230048567300485d")

MongoDB のクエリでは: (person.id = "4baa56f1230048567300485c"、address.id = "4baa56f1230048567300485d"だとして)

db.people.update(
  { "_id" : "4baa56f1230048567300485c" },
  { "$pull" : { "addresses" : { "_id" : "4baa56f1230048567300485d" } } },
  false,
  true
);


永続化は以上です。

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


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

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 でみることができます。


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