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 );
永続化は以上です。
DHH の最新インタビュー紹介
bigthink.com というサイトに、7月22日に記録され、8月3日付けで公開された、DHH の現時点で最新と思われるインタビューがあります。ビデオの他、文章に書き起こしたものがありますので、英語の聞き取りが出来ない人も、読んで内容を把握することができます。
DHH らしい、キレのあるインタビューなので、おもしろいですよ。
以下、プロフィールと各章のタイトル&サブタイトルと質問だけ翻訳・紹介しておきます。各章にリンクを貼ってありますので、質問を読んで気になる部分だけ読みに行くとかでもいいんじゃないでしょうか。
意訳なので保証はありません。「()」は私の注です。
プロフィール
ディヴィッド・ハイネマイヤー・ハンソン(以下 DHH)はデンマーク人のプログラマで、オープンソース・ウェブアプリケーションフレームワーク Ruby on Rails のクリエイターだ。彼はまた、ウェブソフトウェア開発企業 37signals のジェイソン・フリードのパートナーでもある。2005年には、Ruby on Rails 作成者として、グーグルとオライリーの賞、ハッカー・オブ・ザ・イヤーに輝いた。彼とフリードはまた、共著で「Rework」(邦訳:「小さなチーム、大きな仕事」)を著し、ニューヨーク・タイムズベストセラーに選ばれた。「Rework」では、インターネット時代のビジネスの生産性を上げる秘密が書かれている。
シンプルイズベスト
スモールビジネスを成功に導く良書
斬新でシンプルで、会社の作り方、仕事の進め方が提案されている。
純粋にに普通に突き詰めた、シンプルな解かと。
Ruby は コーディングというよりも、人間の思考に近い
DHH は PHP と Java にうんざりしていた。もうほとんど完全にお手上げだった。しかし、Ruby に出会い、全てが変わった。
よいプログラミングは、よい文章を書くことのようだ
「Ruby が共に働く上で特別になる一番大きなポイントは、数行のコードに収められるとても大きな表現力だ」と DHH は言った。
「純粋な」プログラマなんていない
Ruby on Rails のようなアプリがウェブアプリを作る敷居を下げた。プログラマの中には「不潔な大衆」が「美しく、素朴なプログラミングコミュニティ」にはびこったと愚痴る者がいる。DHH はそんな態度は「でたらめ」だと考える。
- Q: Rails は、ウェブアプリを作るのに、技術的な専門的知識の必要性を下げました。専門的知識はもう重要でない?
- Q: プログラミングに関して、どうやったらシンプルになる?
Rails 3が来る!
DHH は昨年 Rails と Merb をマージした。初期の「口論」にもかかわらず、協働の成果—Rails 3—はリリース間近だ。
Microsoft は重要でない
「彼らは僕のすることと何の関係もない」とソフトウェア界の偉人 DHH は言う。しかし、彼は、結集して対抗する「邪悪な帝国」がもはやないのが、なんとなく寂しいと認める。
なぜ、全てがクラウド中心に動いてはいけないか
クラウドコンピューティングは、他を排除する選択ではない。我々は確かに、あるタスクのためにはウェブのすばらしい協調の可能性を利用すべきだが、他のタスクのためには「ローカルの、モダンなコンピューターの、すばらしいグラフィックの力、コンピューティングの力」を利用すべきだ。
- Q: 今のトレンドはクライアントサイド・アプリケーションの方へ向かっています。しかし、Rails は基本的に、サーバサイドを扱っています。Rails は、持続するために、進化する必要がある?
- Q: クラウドコンピューティングが、完全に引き継ぐ?
Apple の App Store は「クソの倉庫」
DHH は Apple の製品を愛している。しかし、iPhone アプリのプログラミング言語の制限方針は「ファシストの行動」だと言う。
オープンソース開発とビジネスの両立方法
オープンソース開発と自分自身のビジネスを成長させることのバランスを取る最も良い方法は、2つを合体させることだ、と DHH は勧める。「自分のビジネスに必要なことを、(オープンソースソフトウェアの)機能として実装している」
- Q: オープンソースソフトウェア開発と自身のビジネスを発展させる必要と、どうやってバランスをとっているのですか?
- Q: 開発者は、オープンソースとプロプライエタリの間で、どう道筋をつけるべきだと思いますか?
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 でみることができます。
ドキュメントについては、以上になります。
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 | MongoHQ や MongoMachine などを使用しているときに、システムのコレクションが接続を許さないせいで、認証の問題が起きた場合に 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
インストールについては以上になります。