データのプリロードと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 クラスには、Server Plugin API メソッド register_topic_preloader_associations があります。これは、ActivityPub プラグインで見られたのと同じパターンを、``ActiveRecord::Associations::Preloader` を使用して適用します。関連付けの配列を渡すと、その作業が行われます。

register_topic_list_preload_user_ids

必要なすべてのトピックを取得するために 1 つの大きなクエリを実行することに加えて、TopicList クラスは、各トピックに必要なすべてのユーザーを取得するためにも 1 つの大きなクエリを実行します。

  • トピックユーザー
  • 最後の投稿者
  • トピックの注目のユーザー
  • トピックの許可されたユーザー

トピックごとに他のユーザーをロードしている場合、この API メソッドを使用すると、ユーザー ID を大きなユーザークエリに含めることができるため、他のユーザーと一緒にデータがプリロードされます。

カテゴリ

サイトのさまざまな場所で使用されるカテゴリのメインリストがあり、Site モデルでロードおよびキャッシュされます。これは厳密な意味ではプリローディングではありませんが、同様のことを行っているため、ここに含めました。

site_all_categories_cache_query modifier

all_categories_cache メソッドには、大きなカテゴリクエリを修正できる Server Plugin 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

Server Plugin API メソッド register_search_topic_eager_load を使用すると、検索で追加のテーブルを Eager Load できます。たとえば、次のようになります。

register_search_topic_eager_load do |opts|
    %i(example_table)
end

カスタムフィールド

カスタムフィールドは、HasCustomFields 関連付けによって強化されています。この関連付けは、カスタムフィールドが関連付けられているモデル、つまり Post、Topic、Category、Group に含まれています。HasCustomFields モジュールには独自のプリローディングシステムがあり、さまざまな方法で使用できます。HasCustomFields プリローディングを使用する最良の方法は、Server Plugin API メソッドを介して行うことです。これらは、これまでに述べたことを考慮すると、比較的自己説明的であるはずです。

register_preloaded_category_custom_fields

add_preloaded_group_custom_field

add_preloaded_topic_list_custom_field

また、ActivityPub プラグインの例で見たように、必要に応じて独自のプリローディングを行うために、HasCustomFields の低レベルメソッドの一部を使用することもできます。その例では、TopicView.on_preloadHasCustomFields 関連付けの preload_custom_fields を使用して、投稿カスタムフィールドをプリロードしています。

TopicView.on_preload do |topic_view|
  ##
  Post.preload_custom_fields(topic_view.posts, activity_pub_post_custom_field_names)
  ##
end
「いいね!」 4