Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

ActiveRecordのカウンターキャッシュを使用したカウンターのキャッシュ

ActiveRecordのカウンターキャッシング機能を使用すると、ページが読み込まれるたびにデータベース内の関連レコードをカウントする代わりに、関連オブジェクトが作成または削除されるたびにカウンターを保存して更新できます。 AppSignal Academyのこのエピソードでは、ActiveRecordでのカウンターのキャッシュについてすべて学びます。

記事と回答を含むブログの典型的な例を見てみましょう。各記事には回答を含めることができます。ブログのインデックスページの各記事のタイトルの横に回答数を表示して、人気を示したいと思います。

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
 
  # ...
end

インデックスページにデータを表示しないため、応答をプリロードする必要はありません。カウンターを表示しているので、各記事の回答数のみに関心があります。コントローラはすべての記事を検索し、それらを@articlesに配置します 使用するビューの変数。

<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
 
<% @articles.each do |article| %>
<article>
  <h1><%= article.title %></h1>
  <p><%= article.description %></p>
  <%= article.responses.size %> responses
</article>
<% end %>

ビューは各記事をループし、そのタイトル、説明、および受信した応答の数を表示します。 article.responses.sizeと呼ぶからです ビューでは、ActiveRecordは、応答ごとにレコード全体をロードするのではなく、関連付けをカウントする必要があることを認識しています。

ヒント#countですが 応答の数を数えるためのより直感的な選択のように聞こえますが、この例では#sizeを使用しています 、#countとして 常にCOUNTを実行します #sizeの間、クエリを実行します 応答がすでにロードされている場合は、クエリをスキップします。

Started GET "/articles" for 127.0.0.1 at 2018-06-14 16:25:36 +0200
Processing by ArticlesController#index as HTML
  Rendering articles/index.html.erb within layouts/application
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
  ↳ app/views/articles/index.html.erb:3
  (0.2ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 2]]
  ↳ app/views/articles/index.html.erb:7
  (0.3ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 3]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 4]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 5]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 6]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 7]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 8]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 9]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 10]]
  ↳ app/views/articles/index.html.erb:7
  (0.1ms)  SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ?  [["article_id", 11]]
  ↳ app/views/articles/index.html.erb:7
  Rendered articles/index.html.erb within layouts/application (23.1ms)
Completed 200 OK in 52ms (Views: 45.7ms | ActiveRecord: 1.6ms)

ActiveRecordが各記事の応答カウントを個別のクエリにレイジーロードするため、ブログのインデックスをリクエストするとN+1クエリが発生します。

COUNT()の使用 クエリから

記事ごとに余分なクエリを実行しないようにするために、記事と応答テーブルを結合して、単一のクエリで関連付けられた応答をカウントできます。

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.
      joins(:responses).
      select("articles.*", 'COUNT("responses.id") AS responses_count').
      group('articles.id')
  end
 
  # ...
end

この例では、記事クエリの応答を結合し、COUNT("responses.id")を選択します。 応答の数を数えます。製品IDでグループ化して、記事ごとの回答をカウントします。ビューでは、responses_countを使用する必要があります sizeを呼び出す代わりに 応答の関連付けについて。

このソリューションは、最初のクエリをより遅く、より複雑にすることで、余分なクエリを防ぎます。これは、このページのパフォーマンスを最適化するための優れた最初のステップですが、さらに一歩進んでカウンターをキャッシュできるため、すべてのページビューで各応答をカウントする必要はありません。

カウンターキャッシュ

ブログの記事は(願わくば)更新されるよりも頻繁に読まれるので、カウンターキャッシュ このページのクエリをより速く簡単にするための優れた最適化です。

記事が表示されるたびに応答の数をカウントする代わりに、カウンターキャッシュは、各記事のデータベース行に格納されている個別の応答カウンターを保持します。応答が追加または削除されるたびに、カウンターが更新されます。

これにより、クエリの応答を結合しなくても、1つのデータベースクエリで記事のインデックスをレンダリングできます。設定するには、belongs_toのスイッチを切り替えます counter_cacheを設定して関係を設定します オプション。

# app/models/response.rb
class Response
  belongs_to :article, counter_cache: true
end

これには、Articleへのフィールドが必要です responses_countという名前のモデル 。 counter_cache オプションを選択すると、応答が追加または削除されるたびに、そのフィールドの番号が自動的に更新されます。

ヒント :フィールド名は、trueの代わりに記号を使用して上書きできます counter_cacheの値として オプション。

カウントを保存するために、データベースに新しい列を作成します。

$ rails generate migration AddResponsesCountToArticles responses_count:integer
      invoke  active_record
      create    db/migrate/20180618093257_add_responses_count_to_articles.rb
$ rake db:migrate
== 20180618093257 AddResponsesCountToArticles: migrating ======================
-- add_column(:articles, :responses_count, :integer)
  -> 0.0016s
== 20180618093257 AddResponsesCountToArticles: migrated (0.0017s) =============

応答の数がarticlesテーブルにキャッシュされるようになったため、articlesクエリで応答を結合する必要はありません。 Article.allを使用します コントローラ内のすべての記事をフェッチします。

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
 
  # ...
end

Railsは#sizeにカウンターキャッシュを使用することを理解しているため、ビューを変更する必要はありません。 メソッド。

<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
 
<% @articles.each do |article| %>
<article>
  <h1><%= article.title %></h1>
  <p><%= article.description %></p>
  <%= article.responses.size %> responses
</article>
<% end %>

インデックスを再度リクエストすると、1つのクエリが実行されていることがわかります。各記事は回答の数を知っているため、回答テーブルをクエリする必要はまったくありません。

Started GET "/articles" for 127.0.0.1 at 2018-06-14 17:15:23 +0200
Processing by ArticlesController#index as HTML
  Rendering articles/index.html.erb within layouts/application
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
  ↳ app/views/articles/index.html.erb:3
  Rendered articles/index.html.erb within layouts/application (3.5ms)
Completed 200 OK in 42ms (Views: 36.5ms | ActiveRecord: 0.2ms)
スコープ付きアソシエーションのカウンターキャッシュ

ActiveRecordのカウンターキャッシュコールバックは、レコードを作成または破棄するときにのみ起動するため、スコープ付きアソシエーションにカウンターキャッシュを追加することはできません。 *公開された*応答の数だけを数えるなどの高度なケースについては、counter_culturegemを確認してください。

カウンターキャッシュへの入力

カウンターキャッシュより前の記事の場合、デフォルトでは0であるため、カウンターは同期していません。 .reset_countersを使用して、オブジェクトのカウンターを「リセット」できます。 その上でメソッドを実行し、オブジェクトのIDと、カウンターを更新する必要がある関係を渡します。

Article.reset_counters(article.id, :responses)

デプロイ時にこれが本番環境で確実に実行されるように、最後の移行で列を追加した直後に実行される移行に配置します。

$ rails generate migration PopulateArticleResponsesCount --force
      invoke  active_record
      create    db/migrate/20180618093443_populate_article_responses_count.rb

移行では、Article.reset_countersを呼び出します。 記事ごとに、記事のIDと:responsesを渡します 協会の名前として。

# db/migrate/20180618093443_populate_article_responses_count.rb
class PopulateArticleResponsesCount < ActiveRecord::Migration[5.2]
  def up
    Article.find_each do |article|
      Article.reset_counters(article.id, :responses)
    end
  end
end

この移行により、カウンターキャッシュの前に存在していた記事を含む、データベース内のすべての記事のカウントが更新されます。

コールバック

カウンターキャッシュはコールバックを使用してカウンターを更新するため、SQLコマンドを直接実行するメソッド(#deleteを使用する場合など) #destroyの代わりに )カウンターは更新されません。

何らかの理由でそれが発生する状況では、定期的にカウントの同期を維持するRakeタスクまたはバックグラウンドジョブを追加することが理にかなっている場合があります。

namespace :counters do
  task update: :environment do
    Article.find_each do |article|
      Article.reset_counters(article.id, :responses)
    end
  end
end

キャッシュされたカウンター

クエリ内の関連オブジェクトをカウントすることでN+1クエリを防ぐことは役立ちますが、カウンターのキャッシュは、ほとんどのアプリケーションのカウンターを表示するためのさらに高速な方法です。 ActiveRecordの組み込みのキャッシュされたカウンターは非常に役立ち、counter_cultureなどのオプションを使用してより複雑な要件を実現できます。

ActiveRecordのカウンターキャッシュについて質問がありますか? @AppSignalまでお気軽にお知らせください。もちろん、この記事がどのように気に入ったか、または別のテーマについてもっと知りたい場合は、ぜひお知らせください。


  1. エッジキャッシングを使用した5ミリ秒のグローバルRedisレイテンシ

    データベースとクライアントが同じリージョンにある場合、Redisを使用すると1ミリ秒のレイテンシーが簡単になります。ただし、クライアントをグローバルに分散させたい場合は、遅延が100ミリ秒を超えて増加します。これを克服するためにEdgeCachingを構築しました。 エッジキャッシング エッジキャッシングを使用すると、REST応答は、CDNと同様に、世界中のエッジロケーションにキャッシュされます。エッジキャッシングが有効になっている場合、平均で5msのグローバルレイテンシが見られます。 10の異なるリージョンにあるクライアントからのレイテンシー数を記録するベンチマークアプリケーションを参照し

  2. ActiveRecord列挙型を使用した簡単で読み取り可能な属性の作成

    「保留中」、「承認済み」、または「フラグ付き」のいずれかの質問を想像してみてください。または、「自宅」、「オフィス」、「モバイル」、または「ファックス」の電話番号(1982年の場合)。 一部のモデルでは、この種のデータが必要です。 いくつかの異なる値のうちの1つのみを持つことができる属性。そして、その値のセットはほとんど変更されません。 これは、単純なRubyの場合、記号を使用するだけの状況です。 PhoneNumberTypeまたはQuestionStatusモデルとbelongs_toを作成できます これらの価値観を保持するための関係ですが、それだけの価値はないようです。それらをya