#ruby-on-rails #ruby #activerecord
#ruby-on-rails #ruby #activerecord
Вопрос:
Я работаю над плагином для Discourse, что означает, что я могу изменять классы с помощью class_eval, но я не могу изменить схему БД. Для хранения дополнительных данных о тематической модели я могу выполнять объединения с помощью TopicCustomField, который предоставляется для этой цели.
Я могу хранить и извлекать все необходимые мне данные, но когда одновременно загружается много тем, производительность БД неэффективна, потому что мои косвенные данные загружаются один раз для каждой темы отдельно. Было бы намного лучше, если бы эти данные загружались все сразу для каждой темы, как это может произойти при использовании предварительной загрузки или включения.
Например, каждая тема имеет topic_guid и набор parent_guid (хранятся в одной строке с тире, потому что порядок важен). Эти parent_guids указывают как на topic_guids другой темы, так и на названия других групп.
Я хотел бы иметь возможность написать что-то вроде:
has_many :topic_custom_fields has_many:parent_guids, -> { где(имя: 'parent_guids').pluck(:значение).сначала }, :через => :topic_custom_fields имеет множество :родительских групп, имя_класса: 'Group', primary_key: :parent_guids, foreign_key: :name
Но это:through жалуется на невозможность найти ассоциацию «:parent_guids» в TopicCustomField, а primary_key фактически не принимает ассоциацию вместо столбца DB.
Я также пробовал следующее, но предложения :through не могут использовать функции в качестве ассоциаций.
has_many :topic_custom_fields do
def parent_guids
parent_guids_str = where(name: PARENT_GUIDS_FIELD_NAME).pluck(:value).first
return [] unless parent_guids_str
parent_guids_str.split('-').delete_if { |s| s.length == 0 }
end
def parent_groups
Group.where(name: parent_guids)
end
end
has_many :parent_guids, :through => :topic_custom_fields
has_many :parent_groups, :through => :topic_custom_fields
Использование Rails 4.2.7.1
Комментарии:
1. Похоже, общая стратегия для пользовательской предварительной загрузки здесь: mrbrdo.wordpress.com/2013/09/25 /…
Ответ №1:
На самом деле, through
параметр rails associations заключается в том, чтобы установить связь «многие ко многим» с моделью, проходящей «через» другую модель:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Так что вы не можете сделать
has_many :parent_guids, :through => :topic_custom_fields
поскольку ParentGuid
это не модель, связанная TopicCustomFields
с. Кроме того, передача блока has_many
только для расширения ассоциации с новыми методами, такими как те, которые rails уже предоставляют вам, например topic_custom_fields.create
, topic_custom_fields.build
, и т.д.
Почему бы вам не определить методы внутри блока во втором примере в Topic
классе для извлечения групп? Есть ли что-то, что вы хотите, что было бы невозможно только с помощью методов?
Обновить
Ну, я не думаю, что в этом случае возможно добиться такой же улучшенной производительности, поскольку идентификаторы групп все еще должны обрабатываться topic_custom_fields
, а улучшенная производительность достигается с помощью объединений. Может быть, сложная комбинация preload
, where
и references
может сделать трюк, но я не знаю, возможно ли это.
Вместо этого вы могли бы попытаться свести к минимуму вызовы БД, возможно, собрав все parent_guids перед запросом групп.
Комментарии:
1. У меня действительно была такая работа, но это приводит к снижению производительности, потому что эти методы оцениваются индивидуально для каждой темы. Для ассоциаций я могу выполнить topics.preload(:topic_custom_fields), который загружает все соответствующие данные пользовательских полей одним запросом для всех тем одновременно. Я хотел бы аналогичным образом иметь возможность выполнять topics.preload(:parent_guids), preload(:parent_groups) и т.д… Я знаю, что through так не работает, поэтому я хотел бы найти альтернативу, которая может привести к такому же повышению производительности!
Ответ №2:
Я надеюсь, что есть более элегантное решение, но это то, что я сделал для эффективной предварительной загрузки моих данных. Это должно быть довольно легко распространить на другие приложения.
Я изменяю exec_queries отношения, который вызывает другие функции предварительной загрузки.
ActiveRecord::Relation.class_eval do
attr_accessor :preload_funcs
old_exec_queries = self.instance_method(:exec_queries)
define_method(:exec_queries) do |amp;block|
records = old_exec_queries.bind(self).call(amp;block)
if preload_funcs
preload_funcs.each do |func|
func.call(self, records)
end
end
records
end
end
К теме я добавил:
has_many :topic_custom_fields
attr_accessor :parent_groups
def parent_guids
parent_guids_str = topic_custom_fields.select { |a| a.name == PARENT_GUIDS_FIELD_NAME }.first
return [] unless parent_guids_str
parent_guids_str.value.split('-').delete_if { |s| s.length == 0 }
end
И затем, чтобы предварительно загрузить parent_groups, я делаю:
def preload_parent_groups(topics)
topics.preload_funcs ||= []
topics.preload_funcs <<= Proc.new do |association, records|
parent_guidss = association.map {|t| t.parent_guids}.flatten
parent_groupss = Group.where(name: parent_guidss).to_a
records.each do |t|
t.parent_groups = t.parent_guids.map {|guid| parent_groupss.select {|group| group.name == guid }.first}
end
end
topics
end
И, наконец, я добавляю предварительные загрузчики в свой запрос отношения:
result = result.preload(:topic_custom_fields)
result = preload_parent_groups(result)