预加载数据并处理 N+1 查询问题

在使用 Discourse rails 应用程序时,无论是构建插件还是向 discourse/discourse 提交拉取请求,都会遇到 N+1 查询问题。本主题将介绍如何通过数据预加载来解决这些问题,并保持 Discourse 的高性能。

Rails 中的 N+1 查询问题

首先,如果您还不熟悉 N+1 查询问题以及 Rails 如何解决它们,请参阅指南的这一部分。

主题

加载主题时需要注意的 N+1 问题是加载与帖子关联的表。现在,单个主题序列化后的数据在 TopicView 类中准备并预加载。该类有一个 on_preload 钩子,允许您在运行主要查询之前预加载关联表。在 ActivityPub 插件中有一个 TopicView.on_preload 的使用示例

此钩子的使用既预加载了自定义字段(我们将在下面介绍),又为主题中的帖子加载了其他关联。您会注意到它使用了 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 方法允许您将他们的用户 ID 包含在大的用户查询中,以便与其他人一起预加载他们的数据。

分类

站点在各种地方使用的分类有一个主列表,该列表在 Site 模型中加载和缓存。虽然这在严格意义上不是预加载,但它做了类似的事情,所以我认为应该在这里包含它。

site_all_categories_cache_query modifier

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

搜索

搜索预加载在代码中也没有被严格描述,但它在查询方面也做了类似的事情,即急切加载。

register_search_topic_eager_load

服务器插件 API 方法 register_search_topic_eager_load 允许您在搜索中急切加载其他表。例如:

register_search_topic_eager_load do |opts|
    %i(example_table)
end

自定义字段

自定义字段由 HasCustomFields 关注点提供支持。该关注点包含在具有关联自定义字段的模型中,即 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 插件示例中看到的那样。在该示例中,HasCustomFields 关注点的 preload_custom_fields 用于在 TopicView.on_preload 中预加载帖子自定义字段。

TopicView.on_preload do |topic_view|
  ##
  Post.preload_custom_fields(topic_view.posts, activity_pub_post_custom_field_names)
  ##
end
4 个赞