Ruby
Rails
ActiveRecord
mongoid

Rails ガイドを読みながら Mongoid のクエリインターフェイスを Active Record と比較してみた

More than 1 year has passed since last update.

はじめに

Mongoid は Ruby もしくは Rails から MongoDB を使う場合の標準的な OR マッパー(公式には Object-Document-Mapper)です。

Mongoid は Mongoid::Criteria による Active Record によく似たクエリインターフェースがあります。しかし、Active Record にはあって Mongoid にないメソッドや、同じメソッド名でも引数や例外の有無が違うという事があり、混乱することがあります。

そこで、Rails ガイドの Active Record クエリインターフェイス の内容について Mongoid から見た Active Record との違いを確認してみました。

もともと Mongoid と Active Record は継承関係や include しているモジュールが大きく異なるため、ここで取り上げていない違いはたくさんありますがご了承ください。

Mongoid から見た Active Record との違い

take がない

Active Record の take はレコードを1つ取り出しますが、Mongoid にはありません。

ActiveRecord
> ActiveRecordModel.take
=> #<ActiveRecordModel:0x007f96520a1ed8
 id: 1, ...
Mongoid
> MongoidModel.take
NoMethodError: undefined method `take' for MongoidModel:Class

first, last に引数を指定できない

first は主キー順の最初のデータ、last は最後のデータを取り出します。

Active Record ではこれらのメソッドに引数を指定するとその最大値の配列を返します。

ActiveRecord
> ActiveRecordModel.first(3)
=> [#<ActiveRecordModel:0x007f96520a1ed8
 id: 1, ...],
  #<ActiveRecordModel:0x007f96528a1fe8
  id: 2, ...],
 #<ActiveRecordModel:0x007f96521dccd0
  id: 3, ...]]
> ActiveRecordModel.last(3)
=> [#<ActiveRecordModel:0x007f9652cf27a0
 id: 6, ...],
  #<ActiveRecordModel:0x007f9652cf28e0
  id: 7, ...],
 #<ActiveRecordModel:0x007f9652cf2a20
  id: 8, ...]]

Mongoid では引数を指定することが出来ません。

Mongoid
> MongoidModel.first(3)
ArgumentError: wrong number of arguments (given 1, expected 0)
> MongoidModel.last(3)
ArgumentError: wrong number of arguments (given 1, expected 0)

find_by でヒットしなかった場合に例外が発生する

find_by は与えられた条件にマッチするデータのうち、最初のデータを返します。

Active Record ではヒットしなかった場合 nil を返します。

ActiveRecord
> ActiveRecordModel.find_by(name: "xxx")
=> nil

Mongoid ではヒットせず、さらに Mongoid.raise_not_found_errortrue だとMongoid::Errors::DocumentNotFound 例外が発生します。

Mongoid
> MongoidModel.find_by name: "unknown"
[MONGO]  { "find" => "clients", "filter" => { "name" => "unknown" }, "sort" => { "_id" => 1 } } (23.6 ms)
Mongoid::Errors::DocumentNotFound:
message:
  Document not found for class MongoidModel with attributes {:name=>"unknown"}.
summary:
summary:
  When calling StationMaster.find_by with a hash of attributes, all attributes provided must match a document in the database or this error will be raised.
resolution:
(省略)

where の検索文字列埋め込み

Active Record では、where の引数に ? や変数を埋め込んだ文字列を渡して検索ができます。

ActiveRecord
# ?を使った場合
> Client.where("orders_count = ?", params[:orders])
# プレースホルダを使った場合
> Client.where("orders_count = #{params[:orders]}")

Mongoid で同じように実行するとエラーになります。

Mongoid
# ?を使った場合
> ActiveRecordModel.where("orders_count = ?", params[:orders])
ArgumentError: wrong number of arguments (given 2, expected 1)
# プレースホルダを使った場合
> ActiveRecordModel.where("orders_count = :orders_count", {orders_count: params[:orders]})
ArgumentError: wrong number of arguments (given 2, expected 1)

代わりに for_js を使用できます。

Mongoid
> MongoidModel.for_js("this.name = param", param: "Tool").first
=> #<Mongoid::Criteria
  selector: {"$where"=>#<BSON::CodeWithScope:0x007f9652b39738 @javascript="this.name = param", @scope={:param=>"Tool"}>}
  options:  {:sort=>{"_id"=>1}}
  class:    MongoidModel
  embedded: false>

where だと次の様な指定もできます。

Mongoid
> MongoidModel.where("orders_count == #{params[:orders}")
=> #<Mongoid::Criteria
  selector: {"$where"=>"orders_count == xxxx"}
  options:  {:sort=>{"_id"=>1}}
  class:    MongoidModel
  embedded: false>

ちなみに Rails ガイドにも書かれているとおり、この埋込み例はパラメータをそのまま渡しているので注意が必要です。通常は避けるべきです。詳しくはRuby on Railsセキュリティガイドを参照してください。

join,joins,left_outer_joins はない

そもそも MongoDB ではコレクション(RDB のテーブルに該当するもの)結合の機能がないため、JOIN に関するメソッドもありません。

Mongoid
> MongoidModel.respond_to?(:join)
false
> MongoidModel.respond_to?(:joins)
false
> MongoidModel.respond_to?(:left_outer_joins)
false

MongoDB 3.2 で JOIN に似た $lookup が追加されましたが、今のところ Mongoid では応していません。

サブセット条件は any_of(または or)

Active Record の場合、where の条件ハッシュに配列を指定して、SQL の IN 式を作ることができます。

ActiveRecord
> ActiveRecordModel.where(orders_count: [1,3,5]).size
=> 3

これに対し、Mongoid では次の様に1つずつ条件ハッシュの配列を any_of もしくは or に渡します。

Mongoid
> MongoidModel.any_of([{orders_count: 1} ,{orders_count: 3}, {orders_count: 5}])
=> 3
# or は any_of のエイリアス
> MongoidModel.or([{orders_count: 1} ,{orders_count: 3}, {orders_count: 5}])
=> 3

where.not がない

Active Record の否定条件は where.not で表現できますが、Mongoid では

Mongoid
> MongoidModel.not_in(id: [1])

もしくは

Mongoid
> MongoidModel.where(:id.nin => [1])

になります。

特定のフィールドを取り出す only

Active Record では特定のレコードのフィールドを取り出すのに select を使います。

ActiveRecord
ActiveRecordModel.select(:id).first
#<ActiveRecordModel:0x00000015dae6d8> {
    :id => 1
}

Mongoid では only を使います。

Mongoid
> MongoidModel.only(:id).first
#<MongoidModel:0x00000016a906a8> {
    :_id => 1
}

find_by_フィールド名 がない

Active Record では、テーブルに定義されたフィールドに対して自動的に検索メソッドが追加されます。

例えば Client モデルに name というフィールドがあった場合、自動的に find_by_name が追加されます。引数に指定した値をそのフィールドで検索してくれる便利な機能ですが、Mongoid ではこの仕組みが存在しません。

SQL による検索はできない

MongoDB には SQL がないため、Active Record に存在する次のメソッドもありません。

ids がない

Active Record では、テーブルの主キーを使用するリレーションのIDをすべて取り出す ids がありますが、Mongoid にはありません。

Mongoid
> MongoidModel.respond_to?(:ids)
=> false

必要であれば代わりに pluck(:_id) を使う事になると思います。

Mongoid
> MongoidModel.pluck(:_id)

exists? に引数を指定できない

Active Record では、テーブルにデータが存在するかを確認する exists? があり、引数に検索条件を指定する事ができます。メソッド名の ? から推測できるように、戻り値は Boolean です。

ActiveRecord
> ActiveRecordModel.exists(3)
=> true

Mongoid では exists? はありますが、引数を指定することが出来ません。

Mongoid
> MongoidModel.exists?
=> true
> MongoidModel.exists?(3)
ArgumentError: wrong number of arguments (given 1, expected 0)

any?, many? がない

Active Record では、1件以上レコードが存在するかを判定する any? と2件以上レコードが存在するかを判定する many? がありますが、Mongoid にはこれらのメソッドがありません。

ActiveRecord
> ActiveRecordModel.find(:all).many? { |model| model.price <= 1000000 }
=> false
> ActiveRecordModel.find(:all).any? { |model| model.price <= 1000000 }
=> true
Mongoid
> MongoidModel.find(:all).any? { |model| model.price <= 1000000 }
NoMethodError: undefined method `any?' for MongoidModel:Class
> MongoidModel.find(:all).many? { |model| model.price <= 1000000 }
NoMethodError: undefined method `many?' for MongoidModel:Class

count? に引数を指定できない

Active Record では、引数に指定したフィールドが値を持つレコードの合計を返す count? がありますが、Mongoid では引数を指定できません。

ActiveRecord
> ActiveRecordModel.count(:age)
=> 10
Mongoid
> Mongoid.count(:age)
ArgumentError: wrong number of arguments (given 1, expected 0)

平均、最小値、最大値

Active Record では、平均を返す average 、最小値を返す minimum 、最大値を返す maximum があります。

ActiveRecord
> ActiveRecordModel.average("age")
=> 35.8
> ActiveRecordModel.minimum("points")
=> 10
> ActiveRecordModel.maximum("points")
=> 44

Mongoid ではそれぞれ avgminmax が対応します。

Mongoid
> MongoidModel.avg("age")
=> 21.304196978175714
> MongoidModel.minimum("age")
=> 1
> MongoidModel.maximum("age")
=> 32

なぜ同じ名前にしなかったのでしょうか。

おまけ

メソッドの定義場所を調べる

RubyMine のコードジャンプ機能が便利で大体正しい定義へジャンプ・表示できます。同名の定義がヒットした場合には、ファイルパスと一緒に一覧が表示されます。

rubymine.png

他には次の方法があります。

Mongoid は YARD によるドキュメントが採用されているので、ここから確認することもできます。

yard.png