Как заставить рельсы не использовать кешированный результат для 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