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 # ブリザード社のゲームをセーブしたプレーヤーを返します


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