Почему не принадлежат_позволяет / имеет_мани с inverse_of, автоматически увлажняют друг друга? - PullRequest
1 голос
/ 10 октября 2019

Есть ли способ гарантировать, что обе стороны ассоциации 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 без перезагрузки родительского элемента ,и наоборот.

Однако на практике мне кажется, что мне нужно перезагрузить родительский элемент, чтобы увидеть обратную сторону гидрата ассоциации:

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>]>

Как ни странно, второй тест, приведенный ниже, проходит успешно (belongs_to автоматически гидратируется в памяти, когда изменяется сторона has_many), но первый сбой. Это заставляет меня задуматься о том, правильно ли настроена ассоциация.

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

(см. Полные тесты здесь .)

1 Ответ

3 голосов
/ 10 октября 2019

Я ожидал, что, как только я назначу родителя ребенку, этот ребенок автоматически появится на родительской стороне под детьми без перезагрузки родителя

На самом деле есть два вопроса:

1) Если ребенок автоматически появится на стороне родителей, прежде чем информация о родителе ребенка будет сохранена в БД (до child.save!)

Я думаю, что это не должнои есть причины для этого. Для правильного отображения дочерних элементов в этом случае Rails должен иметь возможность объединять два массива: 1) дочерние элементы, вычисленные с помощью inverse_of (еще не сохраненные в БД) и 2) дочерние элементы, которые уже сохранены в БД. Это может привести к конфликтам и недопониманию.

2) Если ребенок автоматически появляется на стороне родителей после того, как информация о родителе ребенка сохраняется в БД (после 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) Если в этом случае была пересчитана кэшированная ассоциация при втором вызове?

Я думаю, что так и должно быть, но, может быть, слишком сложно и дорого учитывать все подобные случаи.


Странно, но второй тест, который приведен ниже, проходит успешно (сторона 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 ориентирована на правильную загрузку записей, а не на изменение уже загруженных записей. Однако последний работает в некоторых случаях.

...