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]/
オプション:
与えられた値が含まれるかを検証します。
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")
同じフィールドに対する複数の式
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 でみることができます。
ドキュメントについては、以上になります。