追踪Mongoid的has_one关联的令人关注的行为

在下面的模型中进行验证。
(虽然我觉得每个项目有一个所有者的关系有些微妙,但在这里我们将其置之不理)。

class Item
  include Mongoid::Document

  field :name

  has_one :owner
end
class Owner
  include Mongoid::Document

  field :name

  belongs_to :item
end

创建数据

准备一份物品和一个拥有者,然后设置关联关系。

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") }

更换所有者

添加一个所有者,并将先前创建的物品的所有者更改为新创建的所有者。

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") }

你明白了吗?新旧两个所有者都拥有相同的item_id。

确认物品的所有者

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

对比物品和所有者的ID的结果也相同。

通过重新加载项目来解决此不一致。

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')>

物品的所有者应该从约翰变为肯,但似乎又恢复回原来的状态。

避免这个问题的方法

基本上,對於 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 的名称,但我个人认为无论哪种方法都可以自然地执行,这样做我会感到很高兴。

如果数据结构的设计可以做得很好,我觉得可以通过给这个关系中所有者拥有的item_id加上唯一索引来避免冲突。

广告
将在 10 秒后关闭
bannerAds