Is there a way to ensure that both sides of a belongs_to + has_many association will automatically hydrate according to changes made on the other side of the association, without reloading the other side?
I have a self-join implementation of has_many + belongs to, like so:
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
After configuring inverse_of on both sides of the association, 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, and vice versa.
However in practice it seems that I have to reload the parent in order to see the inverse of the association 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>]>
Strangely, the second test below succeeds (the belongs_to gets automatically hydrated in memory when the has_many side is modified), but the first fails. This leaves me wondering if the association is not properly configured.
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
(See full tests here.)