Предзагрузка данных и решение проблемы N+1 запросов

При работе с Rails-приложением Discourse, будь то создание плагина или внесение изменений в репозиторий discourse/discourse, вы столкнетесь с проблемами N+1 запросов в ряде ситуаций. В этой теме объясняется, как обрабатывать предварительную загрузку данных для решения таких проблем и поддержания высокой производительности Discourse.

Проблемы N+1 запросов в Rails

Во-первых, если вы еще не знакомы с проблемами N+1 запросов и тем, как Rails их решает, ознакомьтесь с этим разделом руководства.

Темы

Проблемы N+1, на которые следует обращать внимание при загрузке темы, возникают при загрузке таблиц, связанных с публикацией. Данные, сериализованные для отдельной темы, подготавливаются и предварительно загружаются в классе TopicView. Этот класс имеет хук on_preload, который позволяет предварительно загружать связанные таблицы перед выполнением основного запроса. Хороший пример использования TopicView.on_preload можно найти в плагине ActivityPub:

Это использование хука позволяет как загружать пользовательские поля (о которых мы поговорим ниже), так и загружать дополнительные ассоциации для публикаций в теме. Обратите внимание, что для этого используется ActiveRecord::Associations::Preloader. Ознакомьтесь с документацией по этому классу:

Списки тем

Проблемы N+1, на которые следует обращать внимание при загрузке списка тем, возникают при загрузке таблиц, связанных с темой. Аналогично классу TopicView для отдельных тем, для списков тем существует класс TopicList. Как и TopicView, класс TopicList также имеет метод on_preload, который можно использовать для подключения к основному запросу списка тем, чтобы загрузить связанные таблицы до того, как запрос будет отправлен в базу данных. Хороший пример этого можно найти в плагине Discourse Assign:

register_topic_preloader_associations

Класс TopicList имеет метод API серверного плагина register_topic_preloader_associations, который по сути применяет тот же паттерн, что и в плагине ActivityPub, используя ActiveRecord::Associations::Preloader. Он выполнит эту работу за вас, если вы передадите ему массив ассоциаций.

register_topic_list_preload_user_ids

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

  • пользователя темы
  • пользователя последней публикации
  • пользователя, выделенного в теме
  • разрешенных пользователей темы

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

Категории

Существует основной список категорий, используемый в различных местах сайта, который загружается и кэшируется в модели Site. Хотя технически это не является предварительной загрузкой в строгом смысле, это выполняет схожую задачу, поэтому я решил включить это здесь.

модификатор site_all_categories_cache_query

Метод all_categories_cache имеет модификатор API серверного плагина, который позволяет изменять большой запрос категорий: site_all_categories_cache_query. Используйте этот модификатор в вашем plugin.rb следующим образом:

register_modifier(:site_all_categories_cache_query) do |query|
   query.where("categories.name LIKE 'Cool%'")
end

Поиск

Предварительная загрузка при поиске также не описывается как таковая в коде, но она выполняет схожую задачу с запросами, то есть eager loading (предварительную загрузку ассоциаций).

register_search_topic_eager_load

Метод API серверного плагина register_search_topic_eager_load позволяет выполнять eager loading дополнительных таблиц в поиске. Например:

register_search_topic_eager_load do |opts|
    %i(example_table)
end

Пользовательские поля

Пользовательские поля работают на основеConcern HasCustomFields. Этот Concern включен в модели, имеющие связанные пользовательские поля, то есть Post, Topic, Category и Group. Модуль HasCustomFields имеет собственную систему предварительной загрузки, которую можно использовать различными способами. Лучший способ использования предварительной загрузки HasCustomFields — через методы API серверного плагина, которые должны быть относительно понятны с учетом всего сказанного выше.

register_preloaded_category_custom_fields

add_preloaded_group_custom_field

add_preloaded_topic_list_custom_field

Вы также можете использовать некоторые методы более низкого уровня в HasCustomFields для собственной предварительной загрузки при необходимости, как мы видели в примере плагина ActivityPub. В этом примере preload_custom_fields Concern HasCustomFields используется для предварительной загрузки пользовательских полей публикаций в TopicView.on_preload.

TopicView.on_preload do |topic_view|
  ##
  Post.preload_custom_fields(topic_view.posts, activity_pub_post_custom_field_names)
  ##
end
4 лайка