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

Mongoid の has_one の気になる挙動を追う

以下のモデルで検証する。
( item が1人の owner を持つ、という関係は微妙な気がするがここでは置いておく)

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

  field :name

  has_one :owner
end
app/models/owner.rb
class Owner
  include Mongoid::Document

  field :name

  belongs_to :item
end

データの作成

item と owner を1件ずつ用意し、リレーションを設定する。

irb(main):001:0> item = Item.create(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334'), "name"=>"item01"}], "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3144df...>}}
MONGODB | [7] localhost:28001 | sandbox.insert | SUCCEEDED | 0.001s
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):002:0> item.owner = Owner.create(name: 'John')
MONGODB | [8] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"owners", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df7ad4821b6205247b04335'), "name"=>"John", "item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}], "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3...
MONGODB | [8] localhost:28001 | sandbox.insert | SUCCEEDED | 0.027s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

mongo shell からは以下のように見える。

> db.items.find()
{ "_id" : ObjectId("5df7ad2c21b6205247b04334"), "name" : "item01" }
> db.owners.find()
{ "_id" : ObjectId("5df7ad4821b6205247b04335"), "name" : "John", "item_id" : ObjectId("5df7ad2c21b6205247b04334") }

owner を変更する

owner を1件追加し、先程作成した item の owner を新しく作った owner に変更する。

irb(main):003:0> item.owner = Owner.create(name: 'Tom')
MONGODB | [9] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"owners", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df7ae8721b6205247b04336'), "name"=>"Tom", "item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}], "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b31...
MONGODB | [9] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7ae8721b6205247b04336, name: "Tom", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

この状態で mongo shell からデータを確認すると以下のように見える。

> db.items.find()
{ "_id" : ObjectId("5df7ad2c21b6205247b04334"), "name" : "item01" }
> db.owners.find()
{ "_id" : ObjectId("5df7ad4821b6205247b04335"), "name" : "John", "item_id" : ObjectId("5df7ad2c21b6205247b04334") }
{ "_id" : ObjectId("5df7ae8721b6205247b04336"), "name" : "Tom", "item_id" : ObjectId("5df7ad2c21b6205247b04334") }

おわかりいただけただろうか。
新旧2件の owner はいずれも同じ item_id を持っている。

item が持つ owner を確認する

irb(main):004:0> item
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):005:0> item.owner
=> #<Owner _id: 5df7ae8721b6205247b04336, name: "Tom", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>
irb(main):006:0> suspected_item = Item.find_by(name: 'item01')
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3144df...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):007:0> suspected_item.owner
MONGODB | [11] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3144df...>}}
MONGODB | [11] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

owner を設定する際に用いた変数 item と新規に find_by で取得した suspected_item はどちらも同じドキュメントを参照している。
しかし、 suspected_item の owner は更新する前の状態になっている。

irb(main):008:0> item.id == suspected_item.id
=> true
irb(main):009:0> item.owner.id == suspected_item.owner.id
=> false

item と owner それぞれの id を比較した結果も同様。

この不一致は item を reload することで解消する。

irb(main):010:0> item.reload
MONGODB | [12] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3144df...>}}
MONGODB | [12] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):011:0> item.owner
MONGODB | [13] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200973173640 type=uuid data=0x2bf6fc2b4b3144df...>}}
MONGODB | [13] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

要約

つまり、 has_one の対象を更新ではなく変更すると操作手順次第で不整合が起きる といえる。
問題の現象を簡易に表現すると以下のようになる。

irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200374997080 type=uuid data=0xa133eef4e6bd4947...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.003s
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):002:0> item.owner
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200374997080 type=uuid data=0xa133eef4e6bd4947...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>
irb(main):003:0> item.owner = Owner.new(name: 'Ken')
MONGODB | [9] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"owners", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df7b22921b6208637b04334'), "name"=>"Ken", "item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}], "lsid"=>{"id"=><BSON::Binary:0x70200374997080 type=uuid data=0xa133eef4e6bd...
MONGODB | [9] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7b22921b6208637b04334, name: "Ken", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>
irb(main):004:0> item.owner
=> #<Owner _id: 5df7b22921b6208637b04334, name: "Ken", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>
irb(main):005:0> item.reload.owner
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "lsid"=>{"id"=><BSON::Binary:0x70200374997080 type=uuid data=0xa133eef4e6bd4947...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [11] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200374997080 type=uuid data=0xa133eef4e6bd4947...>}}
MONGODB | [11] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

item の owner を John から Ken に変更したはずなのに元に戻っている、ように見える。

この問題を避ける方法

基本的には has_one の対象については、別のドキュメントを格納するのではなく、既存のドキュメントを変更するのが良さそう。
つまり、以下のようにすれば良い。

irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200373471320 type=uuid data=0x138828941f17407c...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df7ad2c21b6205247b04334, name: "item01">
irb(main):002:0> item.owner
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200373471320 type=uuid data=0x138828941f17407c...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "John", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>
irb(main):003:0> item.owner.name = 'Tom'
=> "Tom"
irb(main):004:0> item.owner.save
MONGODB | [9] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"owners", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df7ad4821b6205247b04335')}, "u"=>{"$set"=>{"name"=>"Tom"}}}], "lsid"=>{"id"=><BSON::Binary:0x70200373471320 type=uuid data=0x138828941f17407c...>}}
MONGODB | [9] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> true
irb(main):005:0> item.reload.owner
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "lsid"=>{"id"=><BSON::Binary:0x70200373471320 type=uuid data=0x138828941f17407c...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [11] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"owners", "filter"=>{"item_id"=>BSON::ObjectId('5df7ad2c21b6205247b04334')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200373471320 type=uuid data=0x138828941f17407c...>}}
MONGODB | [11] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Owner _id: 5df7ad4821b6205247b04335, name: "Tom", item_id: BSON::ObjectId('5df7ad2c21b6205247b04334')>

(しかし、この例における 別の owner に変更するowner の名前を変更する は意味が違うので、どちらでやっても自然な挙動をしてくれると嬉しいと個人的には思う)

データ構造の設計がしっかりできていれば、このリレーションで owner が持つ item_id は一意にできると思うので unique index をつけることで回避することもできる。

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
ユーザーは見つかりませんでした