Как заставить рельсы не использовать кешированный результат для has_many через отношения?

У меня есть следующие три модели (упрощенно):

class A < ActiveRecord::Base
  has_many :bs
  has_many :cs, :through => :bs
end

class B < ActiveRecord::Base
  belongs_to :a
  has_many :cs
end

class C < ActiveRecord::Base
  belongs_to :b
end

Кажется, что A.cs получает кешированный первый раз, когда он используется (для каждого объекта), когда я действительно этого не хочу.

Здесь консольный сеанс, который подчеркивает проблему (пух был отредактирован)

Во-первых, как он должен работать

rails console
001 > b = B.create
002 > c = C.new
003 > c.b = b
004 > c.save
005 > a = A.create
006 > a.bs << b
007 > a.cs
=> [#<C id: 1, b_id: 1>]

Это действительно так, как вы ожидали. A.cs идет хорошо через отношение a.bs.

И теперь для кеширующих ярости

008 > a2 = A.create
009 > a2.cs
=> []
010 > a2.bs << b
011 > a2.cs
=> []

Итак, первый вызов a2.cs(приводящий к запросу db) вполне корректно возвращает Cs. Второй вызов, однако, показывает отчетливый недостаток Cs, хотя они должны быть хорошими (там не было запросов db).

И просто проверить мое здравомыслие не виноват

012 > A.find(a2.id).cs
=> [#<C id: 1, b_id: 1>]

Опять же, был выполнен запрос db, чтобы получить как запись A, так и связанные с ней.

Итак, вернемся к вопросу: как заставить рельсы не использовать кешированный результат? Я мог бы смириться с этим решением (как показано на шаге 12), но поскольку это приведет к дополнительным двум запросам, когда требуется только один, я бы предпочел не делать этого.

Ответ 1

Я сделал еще несколько исследований по этой проблеме. Хотя использование clear_association_cache было достаточно удобным, добавив его после каждой операции, которая недействительна, кеш не чувствовал СУХОЙ. Я думал, Rails должен уметь отслеживать это. К счастью, есть способ!

Я буду использовать ваши примерные модели: A (имеет много B, имеет множество C до B), B (принадлежит A, имеет много C) и C (принадлежит B).

Нам нужно использовать опцию touch: true для метода belongs_to. Этот метод обновляет атрибут updated_at родительской модели, но, что более важно, он также вызывает обратный вызов after_touch. Этот обратный вызов позволяет нам автоматически очищать кеш ассоциации для любого экземпляра A всякий раз, когда соответствующий экземпляр B или C модифицируется, создается или уничтожается.

Сначала измените вызов метода belongs_to для B и C, добавив touch:true

class B < ActiveRecord::Base
  belongs_to :a, touch: true
  has_many   :cs
end

class C < ActiveRecord::Base
  belongs_to :b, touch: true
end

Затем добавьте обратный вызов after_touch к A

class A < ActiveRecord::Base
  has_many :bs
  has_many :cs, through: :bs

  after_touch :clear_association_cache
end

Теперь мы можем безопасно взломать, создав всевозможные методы, которые изменяют/создают/уничтожают экземпляры B и C, а экземпляр A, к которому они принадлежат, автоматически обновит свой кеш, не задумываясь о том, чтобы вызовите clear_association_cache повсюду.

В зависимости от того, как вы используете модель B, вы можете также добавить туда обратный вызов after_touch.

Документация для параметров belongs_to и обратных вызовов ActiveRecord:

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to

Надеюсь, это поможет!

Ответ 2

(Редактировать: см. ответ Дэниела Уолтрипа, он намного лучше моего)

Итак, после ввода всего этого и просто проверки чего-то не связанного, мои глаза произошли в разделе "3.1 Управление кешированием" руководства по основам ассоциации.

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

Но что, если вы хотите перезагрузить кеш, поскольку данные могли быть изменено какой-либо другой частью приложения? Просто верните вызов ассоциации:

013 > a2.cs(true)
C Load (0.2ms)  SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]

Итак, мораль истории: RTFM; все это.

Изменить: Таким образом, необходимость размещать true по всему месту, вероятно, не такая хорошая вещь, как кеш будет обойти, даже если это не нужно. Решение, предложенное в комментариях Даниэлем Уолтрипом, намного лучше: используйте clear_association_cache

013 > a2.clear_association_cache
014 > a2.cs
C Load (0.2ms)  SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]

Итак, теперь мы не только должны RTFM, мы также должны искать код для :nodoc: s!

Ответ 3

Все методы ассоциации построены вокруг кеширования, что позволяет получить результат последнего запроса для дальнейших операций. Кэш даже разделяется между методами. Например:

customer.orders                 # retrieves orders from the database
customer.orders.size            # uses the cached copy of orders
customer.orders.empty?          # uses the cached copy of orders

Но что, если вы хотите перезагрузить кеш, поскольку данные могли быть изменены какой-либо другой частью приложения? Просто верните вызов ассоциации:

customer.orders                 # retrieves orders from the database
customer.orders.size            # uses the cached copy of orders
customer.orders(true).empty?    # discards the cached copy of orders
                                # and goes back to the database

Источник http://guides.rubyonrails.org/association_basics.html

Ответ 4

Я нашел другой способ отключить кеш запросов. В вашей модели просто добавьте default_scope

class B < ActiveRecord::Base
  belongs_to :a
  has_many   :cs
end

class C < ActiveRecord::Base
  default_scope { } # can be empty too
  belongs_to :b
end

Проверено, что он работает локально. Я нашел это, посмотрев исходный код active_record в active_record/association/association.rb:

# Returns true if statement cache should be skipped on the association reader.
def skip_statement_cache?
  reflection.scope_chain.any?(&:any?) ||
    scope.eager_loading? ||
    klass.current_scope ||
    klass.default_scopes.any? ||
    reflection.source_reflection.active_record.default_scopes.any?
end