为什么 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
在关联的两侧配置 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 似乎专注于正确的记录加载,而不是更改已加载的记录。但是,后者在某些情况下有效。
有没有办法确保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
在关联的两侧配置 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 似乎专注于正确的记录加载,而不是更改已加载的记录。但是,后者在某些情况下有效。