Mongoid は MongoDB での Has and Belongs to Many をサポートしている。
Mongoid の has_and_belongs_to_many
の挙動
まずは多対多のためのモデルを用意する。
class Item
include Mongoid::Document
field :name
has_and_belongs_to_many :categories
end
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 で実現するには以下のようにすればよい。
class Item
include Mongoid::Document
field :name
has_many :categories, class_name: 'ItemCategory'
end
class Category
include Mongoid::Document
field :name
has_many :items, class_name: 'ItemCategory'
end
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
を使うのが良いと考えられる。