【问题标题】:Why don't belongs_to / has_many with inverse_of automatically hydrate each the other side?为什么belongs_to / has_many 和inverse_of 不能自动水合对方?
【发布时间】:2019-10-10 12:51:21
【问题描述】:

有没有办法确保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下的父级下无需重新加载父级,反之亦然。

但在实践中,我似乎必须重新加载父级才能看到关联水合物的逆:

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

【问题讨论】:

    标签: ruby-on-rails ruby activerecord has-many belongs-to


    【解决方案1】:

    我的期望是,一旦我将父母分配给孩子,那么 child 将自动出现在 children 下的 parent 端 无需重新加载父级

    其实有两个问题:

    1) 在关于孩子的父母的信息被持久化到数据库之前(child.save! 之前)

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

    2)当孩子的父母信息被持久化在DB中后(child.save!之后),孩子​​是否应该自动出现在父母端

    当然应该!你甚至不需要inverse_of。由于所有数据都保存在数据库中,因此您应该能够看到它。您没有看到孩子的原因不是因为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) 在这种情况下,是否应该在第二次调用时重新计算缓存的关联?

    我认为应该这样做,但考虑所有这些情况可能太难且太昂贵了。


    奇怪的是,下面的第二个测试成功了(belongs_to 得到 修改has_many边时自动在内存中补水)

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

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

        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 专注于正确加载记录,而不是更改已加载的记录。但是,后者在某些情况下有效。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-04
      • 1970-01-01
      • 2013-11-07
      • 2011-12-20
      • 1970-01-01
      相关资源
      最近更新 更多