Help us understand the problem. What is going on with this article?

Mongoid で多対多する

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

Mongoid の has_and_belongs_to_many の挙動

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

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

  field :name
  has_and_belongs_to_many :categories
end
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件作成する。

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

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

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

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

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 から見ると以下のようになる。

> 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 で実現するには以下のようにすればよい。

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

  field :name
  has_many :categories, class_name: 'ItemCategory'
end
app/models/category.rb
class Category
  include Mongoid::Document

  field :name
  has_many :items, class_name: 'ItemCategory'
end
app/models/item_category.rb
class ItemCategory
  include Mongoid::Document

  belongs_to :item
  belongs_to :category
end

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

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 で見ると以下のようになる。

> 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 を、あるいはその逆を参照するには以下のようにする。

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 を使うのが良いと考えられる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした