Mongoid 全リレーション種別で生成されるクエリを調べたみた

[@koshi_life](https://twitter.com/koshi_life) です。

Mongoidの各リレーションでどのようなクエリが発行されるか高速化するために興味があり、
Mongoidリレーションの公式ドキュメントを読んで、実験した内容の備忘です。

### 前提

- Ruby 2.6.3
- Rails 5.2.3
- Mongo 4.0.3
- Mongoid 7.0.2

# Mongoidのリレーションは5種

記事投稿時点のMongoid(v7)では、
以下5種のリレーションがサポートされています。

[mongoid-Relations](https://docs.mongodb.com/mongoid/current/tutorials/mongoid-relations/) より。

|マクロ|リレーション種別|データ保存先のコレクション|
|:--|:--|:--|
|has_one - belongs_to |1:1|両コレクション|
|embeds_one - embedded_in |1:1|親のみ|
|has_many - belongs_to |1:N|両コレクション|
|embeds_many - embedded_in |1:N|親のみ|
|has_and_belongs_to_many|N:N|両コレクション|

embeds_one/embeds_many が [Railsのリレーション](https://railsguides.jp/association_basics.html) にはなく Mongoid 特有の関連付けです。
Modelは分けたいが、参照/更新タイミングが一緒で別々のコレクションへのクエリを発行させたくない時に利用するリレーションと解釈しました。

# リレーション定義例

適当な例として
フランチャイズチェーンのお店の売上、在庫、商品、オーナー、店長、バイトを管理するための各モデルを定義してみます。

## 関連図

<img width="700px" alt="qiita-20190520_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51734/9e41397c-0473-e27e-89c8-9142d3b88170.png">

## お店モデル

```app/models/store.rb
# お店
class Store
  include Mongoid::Document

  field :name, type: String

  has_one :manager
  embeds_one :owner

  has_many :sales
  has_many :stocks
  has_and_belongs_to_many :part_timers
end
```

## 人関連モデル

```app/models/owner.rb
# オーナー
class Owner
  include Mongoid::Document

  field :name, type: String

  embedded_in :store
end
```

```app/models/manager.rb
# 店長
class Manager
  include Mongoid::Document

  field :name, type: String

  belongs_to :store

  # Index貼るとよいかも (後述)
  index({ store_id: 1 })
end
```

```app/models/part_timer.rb
# バイト
class PartTimer
  include Mongoid::Document

  field :name, type: String

  has_and_belongs_to_many :stores
end
```

## 商品、在庫、売上管理系モデル

```app/models/product.rb
# 商品
class Product
  include Mongoid::Document

  field :name, type: String

  has_one :product_quantity
end
```

```app/models/product_quantity.rb
# 商品と数量の組み合わせを持つ抽象モデル
class ProductQuantity
  include Mongoid::Document

  field :quantity, type: Integer

  belongs_to :product
end
```

```app/models/stock.rb
# 在庫
class Stock < ProductQuantity
  include Mongoid::Document

  belongs_to :store

  # Index貼るとよいかも (後述)
  index({ _type: 1, store_id: 1, product_id: 1 })
end

```

```app/models/sale.rb
# 売上
class Sale
  include Mongoid::Document

  field :price, type: Integer

  belongs_to :store
  embeds_many :sold_products

  # Index貼るとよいかも (後述)
  index({ store_id: 1 })
end
```

```app/models/sold_product.rb
# 売った商品
class SoldProduct < ProductQuantity
  include Mongoid::Document

  field :unit_price, type: Integer

  embedded_in :sale
end
```

## サンプルデータ

```sample_data.rb
module SampleData
  STORE_DATA = [
    Store.new(id: 's001', name: 'Store A', part_timer_ids: %w[p001 p004], owner: Owner.new(name: 'Owner A')),
    Store.new(id: 's002', name: 'Store B', part_timer_ids: %w[p002], owner: Owner.new(name: 'Owner B')),
    Store.new(id: 's003', name: 'Store C', part_timer_ids: %w[p003 p004], owner: Owner.new(name: 'Owner C'))
  ]

  MANAGER_DATA = [
    Manager.new(id: 'm001', name: 'Manager A', store_id: 's001'),
    Manager.new(id: 'm002', name: 'Manager B', store_id: 's002'),
    Manager.new(id: 'm003', name: 'Manager C', store_id: 's003')
  ]

  PRODUCT_DATA = [
    Product.new(id: 'p001', name: 'Product A'),
    Product.new(id: 'p002', name: 'Product B'),
    Product.new(id: 'p003', name: 'Product C')
  ]

  SALE_DATA = [
    Sale.new(
      id: 'sa001001',
      store_id: 's001',
      price: 10,
      sold_products: [SoldProduct.new(product_id: 'p001', quantity: 1, unit_price: 10)]
    ),
    Sale.new(
      id: 'sa001002',
      store_id: 's001',
      price: 40,
      sold_products: [SoldProduct.new(product_id: 'p002', quantity: 2, unit_price: 20)]
    ),
    Sale.new(
      id: 'sa001003',
      store_id: 's001',
      price: 80,
      sold_products: [
        SoldProduct.new(product_id: 'p001', quantity: 2, unit_price: 10),
        SoldProduct.new(product_id: 'p003', quantity: 2, unit_price: 30)
      ]
    )
  ]

  STOCK_DATA = [
    Stock.new(id: 'st001001', product_id: 'p001', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st001002', product_id: 'p002', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st001003', product_id: 'p003', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st002001', product_id: 'p001', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st002002', product_id: 'p002', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st002003', product_id: 'p003', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st003001', product_id: 'p001', quantity: 30, store_id: 's003'),
    Stock.new(id: 'st003002', product_id: 'p002', quantity: 30, store_id: 's003'),
    Stock.new(id: 's003003', product_id: 'p003', quantity: 30, store_id: 's003')
  ]

  PART_TIMER_DATA = [
    PartTimer.new(id: 'p001', name: 'Part A', store_ids: %w[s001 s002]),
    PartTimer.new(id: 'p002', name: 'Part B', store_ids: %w[s002]),
    PartTimer.new(id: 'p003', name: 'Part C', store_ids: %w[s003]),
    PartTimer.new(id: 'p004', name: 'Part D', store_ids: %w[s001 s003])
  ]

  def initialize()
    # Drop Collections
    Store.collection.drop
    Manager.collection.drop
    Product.collection.drop
    Sale.collection.drop
    SoldProduct.collection.drop
    Stock.collection.drop
    PartTimer.collection.drop
    # Insert Data
    STORE_DATA.each(&:save!)
    MANAGER_DATA.each(&:save!)
    PRODUCT_DATA.each(&:save!)
    STOCK_DATA.each(&:save!)
    SALE_DATA.each(&:save!)
    PART_TIMER_DATA.each(&:save!)
  end
  module_function :initialize
end
```

# クエリの確認1

Storeモデルから各モデルへリレーション経由でアクセスした時のMongoへのクエリを確認。

## ruby

```
# Storeの全要素を一括で取得 (Mongoクエリあり★☆★)
irb:001> stores = Store.all.to_a
=> [#<Store _id: s001, name: "Store A", part_timer_ids: ["p001", "p004"]>, #<Store _id: s002, name: "Store B", part_timer_ids: ["p002", "p001"]>, #<Store _id: s003, name: "Store C", part_timer_ids: ["p003", "p004"]>]
irb:002> store1 = stores[0]

# has_one (Mongoクエリあり★☆★)
irb:003> store1.manager
=> #<Manager _id: m001, name: "Manager A", store_id: "s001">

# has_many (Mongoクエリあり★☆★)
irb:004> store1.stocks

# has_many (Mongoクエリあり★☆★)
irb:005> s1_sales = store1.sales
=> [#<Sale _id: sa001001, price: 10, store_id: "s001">, #<Sale _id: sa001002, price: 40, store_id: "s001">, #<Sale _id: sa001003, price: 80, store_id: "s001">]
irb:006> s1_sale1 = s1_sales[0]

# embeds_many (Mongoクエリなし)
irb:007> s1_sale1.sold_products
=> [#<SoldProduct _id: 5ce29a8f48cca4564c626022, quantity: 1, product_id: "p001", _type: "SoldProduct", unit_price: 10>]

# embeds_one (Mongoクエリなし)
irb:008> store1.owner
=> #<Owner _id: 5ce29a8f48cca4564c62601f, name: "Owner A">

# has_and_belongs_to_many (Mongoクエリあり★☆★)
irb:009> store1.part_timers
=> [#<PartTimer _id: p001, name: "Part A", store_ids: ["s001", "s002"]>, #<PartTimer _id: p004, name: "Part D", store_ids: ["s001", "s003"]>]
```

## Mongo クエリ内容

|irb No|ruby|Mongoへのクエリ|インデックス利用有無|
|:--|:--|:--|:--|
|001| `Store.all.to_a` | `{"find"=>"stores", "filter"=>{}, ...}` |無|
|003| `store1.manager` | `{"find"=>"managers", "filter"=>{"store_id"=>"s001"}, ...}` |無|
|004| `store1.stocks` | `{"find"=>"product_quantities", "filter"=>{"store_id"=>"s001", "_type"=>"Stock", ...}` |無|
|005| `store1.sales` | `{"find"=>"sales", "filter"=>{"store_id"=>"s001"}, ...}` |無|
|009| `store1.part_timers` | `{"find"=>"part_timers", "filter"=>{"$and"=>[{"_id"=>{"$in"=>["p001", "p004"]}}]}, ...}` |有|

## 確認できたこと

- embeds_one/embeds_many は取得したドキュメント内に関連データが内包されているので追加で別コレクションへのクエリは発行されない
- has_one/has_many-belongs_to のリレーションは、デフォルトではインデックスが効かないクエリ(フルスキャン)が発行されるので、使用頻度/データ量によってはインデックス作成を検討したほうがよい。
- モデルを継承した場合は、継承元のコレクションに対して`_type`属性付きのドキュメントが作られる。(例だと`Stock`コレクションではなく`ProductQuantities`コレクションに`_type:Stock`で作成された)

# クエリの確認2 (N+1問題は includes を用いる)

[Mongoid::Criteria#includes](https://www.rubydoc.info/github/mongoid/mongoid/Mongoid%2FCriteria%3Aincludes) を用いると一括でリレーションの関連データを取得することができる。

No.001 の rubyコードを以下のように変更すると、それ以降のNo.003,4,5,9 のMongoへのアクセスは発生しない。

```.rb
irb:001> Store.all.includes(:manager, :stocks, :sales, :part_timers).to_a
=> [#<Store _id: s001, name: "Store A", part_timer_ids: ["p001", "p004"]>, #<Store _id: s002, name: "Store B", part_timer_ids: ["p002", "p001"]>, #<Store _id: s003, name: "Store C", part_timer_ids: ["p003", "p004"]>]
```

Mongoのクエリ内容

```.js
{"find"=>"stores", "filter"=>{}, ...}
{"find"=>"managers", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}}, ...}
{"find"=>"product_quantities", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}, "_type"=>"Stock"}, ...}
{"find"=>"sales", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}}, ...}
{"find"=>"part_timers", "filter"=>{"_id"=>{"$in"=>["p001", "p004", "p002", "p003"]}}, ...}
```

includesで指定した各リレーション先のコレクションに対して、inであらかじめ全取得している。

# クエリの確認3 pluck

[Mongoid::Contextual::Mongo#pluck](https://www.rubydoc.info/github/mongoid/mongoid/master/Mongoid%2FContextual%2FMongo:pluck) リレーション関係ないが、効率よいメソッドらしいので。確認してみる。

```.rb
Sale.all.pluck(:id, :price)
```

```.js
{"find"=>"sales", "filter"=>{}, "projection"=>{"_id"=>1, "price"=>1}, ...}
```

射影(projection)でメモリ展開領域が少なくなるのが効率が良い！？と解釈。

# 参考

- [Active Record の関連付け (アソシエーション)](https://railsguides.jp/association_basics.html)
- [Mongoのリレーションについてまとめてみた](https://qiita.com/kkyouhei/items/99efc8eb357f2a4c2348)
- [embeds_*　🆚　has_*どっちを選ぶ!?](https://qiita.com/len/items/6867cd5fae9e683d9039)