LoginSignup
0
0

More than 3 years have passed since last update.

Mongoid で多対多する

Last updated at Posted at 2019-12-11

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

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0