为什么 belongs_to / has_many 和 inverse_of 不自动互相补水?

Why don't belongs_to / has_many with inverse_of automatically hydrate each the other side?

有没有办法确保belongs_to + has_many关联的双方会根据关联另一方所做的更改自动水合,而无需重新加载另一边?

我有一个 has_many + 属于的自连接实现,像这样:

class Activity < ApplicationRecord
  belongs_to :combined_activity_parent, class_name: 'Activity',
             inverse_of: :combined_activity_children, optional: true
  has_many :combined_activity_children, class_name: 'Activity',
           inverse_of: :combined_activity_parent, foreign_key: 'combined_activity_parent_id'
end

(See full model code here.)

在关联的两侧配置 inverse_of 之后,我的期望是,一旦我将父项分配给子项,那么该子项将自动出现在 children [= 下的父项下38=]无需重新加载父级,反之亦然。

然而在实践中,我似乎必须重新加载父级才能看到关联 hydrate 的逆:

irb(main):006:0> parent = Activity.create(friend: Friend.first, region: Region.first, activity_type: ActivityType.first, occur_at: 1.day.from_now)
=> #<Activity id: 63, event: nil, location_id: nil, friend_id: 1, judge_id: nil, occur_at: "2019-10-11 12:31:01", notes: nil, created_at: "2019-10-10 12:31:01", updated_at: "2019-10-10 12:31:01", region_id: 1, confirmed: nil, public_notes: nil, activity_type_id: 1, combined_activity_parent_id: nil>
irb(main):007:0> child = Activity.create(friend: Friend.first, region: Region.first, activity_type: ActivityType.first, occur_at: parent.occur_at + 1.hour)
=> #<Activity id: 64, event: nil, location_id: nil, friend_id: 1, judge_id: nil, occur_at: "2019-10-11 13:31:01", notes: nil, created_at: "2019-10-10 12:31:45", updated_at: "2019-10-10 12:31:45", region_id: 1, confirmed: nil, public_notes: nil, activity_type_id: 1, combined_activity_parent_id: nil>
irb(main):009:0> child.combined_activity_parent = parent
=> #<Activity id: 63, event: nil, location_id: nil, friend_id: 1, judge_id: nil, occur_at: "2019-10-11 12:31:01", notes: nil, created_at: "2019-10-10 12:31:01", updated_at: "2019-10-10 12:31:01", region_id: 1, confirmed: nil, public_notes: nil, activity_type_id: 1, combined_activity_parent_id: nil>
irb(main):011:0> parent.combined_activity_children
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):012:0> child.save!
=> true
irb(main):013:0> parent.combined_activity_children
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):014:0> parent.reload
=> #<Activity id: 63, event: nil, location_id: nil, friend_id: 1, judge_id: nil, occur_at: "2019-10-11 12:31:01", notes: nil, created_at: "2019-10-10 12:31:01", updated_at: "2019-10-10 12:31:01", region_id: 1, confirmed: nil, public_notes: nil, activity_type_id: 1, combined_activity_parent_id: nil>
irb(main):015:0> parent.combined_activity_children
=> #<ActiveRecord::Associations::CollectionProxy [#<Activity id: 64, event: nil, location_id: nil, friend_id: 1, judge_id: nil, occur_at: "2019-10-11 13:31:01", notes: nil, created_at: "2019-10-10 12:31:45", updated_at: "2019-10-10 12:32:35", region_id: 1, confirmed: nil, public_notes: nil, activity_type_id: 1, combined_activity_parent_id: 63>]>

奇怪的是,下面的第二个测试成功了(当 has_many 侧被修改时 belongs_to 在内存中自动水合),但第一个失败。这让我想知道关联是否未正确配置。

context 'associations' do
    it 'should automatically hydrate the other side of a belongs_to' do
        equivalent_time = 1.day.from_now - 1.hour
        activity_1 = create(:activity, occur_at: equivalent_time, public_notes: 'parent activity')
        activity_2 = create(:activity, occur_at: equivalent_time, public_notes: 'child activity',
                            combined_activity_parent: activity_1)
        expect(activity_1.combined_activity_children.first).to eq activity_2
    end

    it 'should automatically hydrate the other side of a has_many' do
        equivalent_time = 1.day.from_now - 1.hour
        activity_1 = create(:activity, occur_at: equivalent_time, public_notes: 'child activity')
        activity_2 = create(:activity, occur_at: equivalent_time, public_notes: 'parent activity',
                            combined_activity_children: [activity_1])
        expect(activity_1.combined_activity_parent).to eq activity_2
    end
end

(查看完整测试 here。)

my expectation was that, once I assign a parent to a child then that child would automatically appear on the parent side under children without reloading the parent

其实有两个问题:

1) 在 child 的 parent 的信息保存到数据库之前,child 是否应该自动出现在 parent 一侧(在 child.save! 之前)

我认为不应该,这是有原因的。为了在这种情况下正确显示 children,Rails 应该能够合并两个数组:1) children 通过 inverse_of 计算(尚未保存在数据库中)和 2) children 已经存在于数据库中。这可能会导致冲突和误解。

2) child是否应该在child的parent信息被持久化到数据库后自动出现在parent这边(在 child.save! 之后)

当然应该!你甚至不需要 inverse_of。由于所有数据都保存在数据库中,您应该能够看到它。你没有看到 children 的原因不是因为 inverse_of 没有工作,而是因为 combined_activity_children 关联在你第一次访问它时(在 child.save!).

parent.children # queries children from DB and cached the result
child.save!
parent.children # the cached result is the same, no queries to DB

那么,还有一个问题:

3) 在这种情况下,是否应该在第二次调用时重新计算缓存的关联?

我认为应该,但考虑到所有这些情况可能太困难和昂贵了。


Strangely, the second test below succeeds (the belongs_to gets automatically hydrated in memory when the has_many side is modified)

当您访问 parent 时,有关它的信息已经保存在数据库中。此外,Rails 处理单个 parent 比处理多个 children 容易得多。因此,inverse_of 没有权利也没有机会在这种情况下不工作。

但是,如果您不仅在创建它之后而且在创建它之前访问 parent,那么检查第二种情况是否有效会很有趣:

    activity_1 = create(:activity)
    expect(activity_1.combined_activity_parent).to eq nil

    activity_2 = create(:activity, combined_activity_children: [activity_1])
    expect(activity_1.combined_activity_parent).to eq activity_2

此外,我想提请注意,您对 inverse_of(对于第一种和第二种情况)的期望从未被记录下来。文档中有一个完全不同的案例 https://guides.rubyonrails.org/association_basics.html#bi-directional-associations

从文档来看,Active Record 似乎专注于正确的记录加载,而不是更改已加载的记录。但是,后者在某些情况下有效。