Mongoid で多対多する

Mongoid は MongoDB での Has and Belongs to Many をサポートしている。

# Mongoid の `has_and_belongs_to_many` の挙動

まずは多対多のためのモデルを用意する。

```rb:app/models/item.rb
class Item
  include Mongoid::Document

  field :name
  has_and_belongs_to_many :categories
end
```

```rb:app/models/category.rb
class Category
  include Mongoid::Document

  field :name
  has_and_belongs_to_many :items
end
```

この2つのモデルは互いを `has_and_belongs_to_many` に設定している。

試しに items を1件作成する。

```rb
irb(main):001:0> item = Item.create(name: 'apple')
=> #<Item _id: 5df1364821b6207e9250c3d4, name: "apple", category_ids: nil>
```

mongo shell からドキュメントが1件作成されていることを確認する。

```js
> db.items.find()
{ "_id" : ObjectId("5df1364821b6207e9250c3d4"), "name" : "apple" }
```

この時点では categories にはドキュメントはない。
次に apple の categories に1件追加する。

```rb

irb(main):002:0> item.categories << Category.new(name: 'fruit')
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df1364821b6207e9250c3d4')}, "u"=>{"$addToSet"=>{"category_ids"=>{"$each"=>[BSON::ObjectId('5df1367d21b6207e9250c3d5')]}}}}], "lsid"=>{"id"=><BSON::Binary:0x70161405581900 ...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
MONGODB | [9] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"categories", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df1367d21b6207e9250c3d5'), "name"=>"fruit", "item_ids"=>[BSON::ObjectId('5df1364821b6207e9250c3d4')]}], "lsid"=>{"id"=><BSON::Binary:0x70161405581900 type=uuid data=0x731...
MONGODB | [9] localhost:28001 | sandbox.insert | SUCCEEDED | 0.011s
=> [#<Category _id: 5df1367d21b6207e9250c3d5, name: "fruit", item_ids: [BSON::ObjectId('5df1364821b6207e9250c3d4')]>]
```

items に対する update と categories に対する insert の両方が行われる。
この結果を mongo shell から見ると以下のようになる。

```js
> db.items.find()
{ "_id" : ObjectId("5df1364821b6207e9250c3d4"), "name" : "apple", "category_ids" : [ ObjectId("5df1367d21b6207e9250c3d5") ] }
> db.categories.find()
{ "_id" : ObjectId("5df1367d21b6207e9250c3d5"), "name" : "fruit", "item_ids" : [ ObjectId("5df1364821b6207e9250c3d4") ] }
```

双方のドキュメントが相手コレクションの id のリストを持つ形になっている。

RDB では Has and Belongs to Many を実現する場合は中間テーブルを用いるのが一般的だが、 Mongoid は中間コレクションは作らないようだ。
（ MongoDB の特性を考えれば Has and Belongs to Many の実現に中間コレクションを作らないのは適切な選択だと思う）

# Mongoid で中間コレクションを用いた多対多の実現

一方で MongoDB であっても中間コレクションを用いる形の多対多を実現することはもちろんできる。
Mongoid で実現するには以下のようにすればよい。

```rb:app/models/item.rb
class Item
  include Mongoid::Document

  field :name
  has_many :categories, class_name: 'ItemCategory'
end
```

```rb:app/models/category.rb
class Category
  include Mongoid::Document

  field :name
  has_many :items, class_name: 'ItemCategory'
end
```

```rb:app/models/item_category.rb
class ItemCategory
  include Mongoid::Document

  belongs_to :item
  belongs_to :category
end
```

この3つのモデルを元に以下のようにデータを作ることができる。

```rb
irb(main):001:0> item = Item.create(name: 'cucumber')
=> #<Item _id: 5df13ab121b620be0550c3d4, name: "cucumber">
irb(main):002:0> category = Category.create(name: 'vegetable')
=> #<Category _id: 5df13b1521b620be0550c3d5, name: "vegetable">
irb(main):003:0> ItemCategory.create(item_id: item.id, category_id: category.id)
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df13ab121b620be0550c3d4')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70161021308820 type=uuid data=0xa3aed61d66164b2d...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"categories", "filter"=>{"_id"=>BSON::ObjectId('5df13b1521b620be0550c3d5')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70161021308820 type=uuid data=0xa3aed61d66164b2d...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
MONGODB | [11] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"item_categories", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df13b7921b620be0550c3d6'), "item_id"=>BSON::ObjectId('5df13ab121b620be0550c3d4'), "category_id"=>BSON::ObjectId('5df13b1521b620be0550c3d5')}], "lsid"=>{"id"=><BSON::...
MONGODB | [11] localhost:28001 | sandbox.insert | SUCCEEDED | 0.008s
=> #<ItemCategory _id: 5df13b7921b620be0550c3d6, item_id: BSON::ObjectId('5df13ab121b620be0550c3d4'), category_id: BSON::ObjectId('5df13b1521b620be0550c3d5')>
```

この状態を mongo shell で見ると以下のようになる。

```js
> db.items.find()
{ "_id" : ObjectId("5df13ab121b620be0550c3d4"), "name" : "cucumber" }
> db.categories.find()
{ "_id" : ObjectId("5df13b1521b620be0550c3d5"), "name" : "vegetable" }
> db.item_categories.find()
{ "_id" : ObjectId("5df13b7921b620be0550c3d6"), "item_id" : ObjectId("5df13ab121b620be0550c3d4"), "category_id" : ObjectId("5df13b1521b620be0550c3d5") }
```

この状態で item から category を、あるいはその逆を参照するには以下のようにする。

```rb
irb(main):004:0> item.categories.first.category
MONGODB | [12] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"categories", "filter"=>{"_id"=>BSON::ObjectId('5df13b1521b620be0550c3d5')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70161021308820 type=uuid data=0xa3aed61d66164b2d...>}}
MONGODB | [12] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Category _id: 5df13b1521b620be0550c3d5, name: "vegetable">
irb(main):005:0> category.items.first.item
MONGODB | [13] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"item_categories", "filter"=>{"category_id"=>BSON::ObjectId('5df13b1521b620be0550c3d5')}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70161021308820 type=uuid data=0xa3aed61d66164b2d...>}}
MONGODB | [13] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [14] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df13ab121b620be0550c3d4')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70161021308820 type=uuid data=0xa3aed61d66164b2d...>}}
MONGODB | [14] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Item _id: 5df13ab121b620be0550c3d4, name: "cucumber">
```

`has_and_belongs_to_many` のケースと見比べると、データの取り回しは中間コレクションがない方が容易に見える。
どうしても中間コレクションが必要だというシーンでなければ基本的には `has_and_belongs_to_many` を使うのが良いと考えられる。