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

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

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

  field :name

  has_one :owner
end
```

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

  field :name

  belongs_to :item
end
```

# データの作成

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

```rb
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 からは以下のように見える。

```js
> 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 に変更する。

```rb
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 からデータを確認すると以下のように見える。

```js
> 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 を確認する

```rb
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 は更新する前の状態になっている。

```rb
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 することで解消する。

```rb
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 の対象を更新ではなく変更すると操作手順次第で不整合が起きる** といえる。
問題の現象を簡易に表現すると以下のようになる。

```rb
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 の対象については、別のドキュメントを格納するのではなく、既存のドキュメントを変更するのが良さそう。
つまり、以下のようにすれば良い。

```rb
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 をつけることで回避することもできる。