Предварительная загрузка цепочки сложных / косвенных функций ActiveRecord / ‘ассоциаций’

#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)