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

# はじめに

[Mongoid](https://github.com/mongodb/mongoid) は Ruby もしくは Rails から [MongoDB](https://www.mongodb.com/) を使う場合の標準的な OR マッパー（公式には Object-Document-Mapper）です。

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

そこで、Rails ガイドの [Active Record クエリインターフェイス](https://railsguides.jp/active_record_querying.html) の内容について **Mongoid から見た Active Record との違い**を確認してみました。

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

# Mongoid から見た Active Record との違い
## `take` がない
Active Record の [`take`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FFinderMethods:take) はレコードを1つ取り出しますが、Mongoid にはありません。

```rb:ActiveRecord
> ActiveRecordModel.take
=> #<ActiveRecordModel:0x007f96520a1ed8
 id: 1, ...
```

```rb:Mongoid
> MongoidModel.take
NoMethodError: undefined method `take' for MongoidModel:Class
```

## `first`, `last` に引数を指定できない
`first` は主キー順の最初のデータ、`last` は最後のデータを取り出します。

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

```rb: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 では引数を指定することが出来ません。

```rb: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)
```

- [Mongoid::Findable#first](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FFindable:first)
- [ActiveRecord::FinderMethods#first](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FFinderMethods:first)

## `find_by` でヒットしなかった場合に例外が発生する
`find_by` は与えられた条件にマッチするデータのうち、最初のデータを返します。

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

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

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

```rb: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:
（省略）
```

- [Mongoid::Findable#find_by](https://railsguides.jp/active_record_querying.html#first)
- [ActiveRecord::FinderMethods#find_by](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FFinderMethods:find_by)

## `where` の検索文字列埋め込み

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

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

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

```rb: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`](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FCriteria:for_js) を使用できます。

```rb: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` だと次の様な指定もできます。

```rb: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セキュリティガイド](https://railsguides.jp/security.html#sql%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)を参照してください。

## `join`,`joins`,`left_outer_joins` はない

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

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

MongoDB 3.2 で JOIN に似た [$lookup](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/) が追加されましたが、今のところ Mongoid では応していません。

## サブセット条件は `any_of`（または `or`)

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

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

これに対し、Mongoid では次の様に1つずつ条件ハッシュの配列を [`any_of`](http://www.rubydoc.info/gems/mongoid/2.4.2/Mongoid%2FCriterion%2FInclusion%3Aany_of) もしくは [`or`](http://www.rubydoc.info/gems/mongoid/2.4.2/Mongoid/Criterion/Inclusion:or) に渡します。

```rb: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 では 

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

もしくは

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

になります。

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

Active Record では特定のレコードのフィールドを取り出すのに [`select`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FQueryMethods:select) を使います。

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

Mongoid では [`only`](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FCriteria:only) を使います。

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

## `find_by_フィールド名` がない

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

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

## SQL による検索はできない

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

- [`find_by_sql`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FQuerying:find_by_sql)
- [`select_all`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FConnectionAdapters%2FQueryCache:select_all)

## `ids` がない

Active Record では、テーブルの主キーを使用するリレーションのIDをすべて取り出す [`ids`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FCalculations:ids) がありますが、Mongoid にはありません。

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

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

```rb:Mongoid
> MongoidModel.pluck(:_id)
```

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

```rb:ActiveRecord
> ActiveRecordModel.exists(3)
=> true
```

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

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

## `any?`, `many?` がない

Active Record では、1件以上レコードが存在するかを判定する [`any?`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FRelation:any%3F) と2件以上レコードが存在するかを判定する [`many?`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FRelation:many%3F) がありますが、Mongoid にはこれらのメソッドがありません。

```rb:ActiveRecord
> ActiveRecordModel.find(:all).many? { |model| model.price <= 1000000 }
=> false
> ActiveRecordModel.find(:all).any? { |model| model.price <= 1000000 }
=> true
```

```rb: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?`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FCalculations:count) がありますが、Mongoid では引数を指定できません。

```rb:ActiveRecord
> ActiveRecordModel.count(:age)
=> 10
```

```rb:Mongoid
> Mongoid.count(:age)
ArgumentError: wrong number of arguments (given 1, expected 0)
```

## 平均、最小値、最大値

Active Record では、平均を返す [`average`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FCalculations:average) 、最小値を返す [`minimum`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FCalculations:minimum) 、最大値を返す [`maximum`](http://www.rubydoc.info/gems/activerecord/ActiveRecord%2FCalculations:maximum) があります。

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

Mongoid ではそれぞれ [`avg`](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FContextual%2FAggregable%2FMongo:avg) 、[`min`](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FContextual%2FAggregable%2FMongo:min) 、[`max`](http://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FContextual%2FAggregable%2FMongo:max) が対応します。

```rb:Mongoid
> MongoidModel.avg("age")
=> 21.304196978175714
> MongoidModel.min("age")
=> 1
> MongoidModel.max("age")
=> 32
```

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

# おまけ

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

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

![rubymine.png](https://qiita-image-store.s3.amazonaws.com/0/36122/0cd50ff3-4a52-da75-1ee9-e20fe6fb2a9f.png)

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

- [Rubyでメソッドの定義場所を見つける方法 \- Qiita](http://qiita.com/jnchito/items/fc8a61b421d026a23ffe)
- [pryで定義場所を見つける方法](http://qiita.com/jnchito/items/fc8a61b421d026a23ffe#comment-29d4a7950a70fca60e42)

Mongoid は [YARD によるドキュメント](http://www.rubydoc.info/github/mongoid/mongoid/)が採用されているので、ここから確認することもできます。

![yard.png](https://qiita-image-store.s3.amazonaws.com/0/36122/d2adbb76-19eb-8a73-bfa6-7b9577f97461.png)