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までお気軽にお知らせください。もちろん、この記事がどのように気に入ったか、または別のテーマについてもっと知りたい場合は、ぜひお知らせください。
-
エッジキャッシングを使用した5ミリ秒のグローバルRedisレイテンシ
データベースとクライアントが同じリージョンにある場合、Redisを使用すると1ミリ秒のレイテンシーが簡単になります。ただし、クライアントをグローバルに分散させたい場合は、遅延が100ミリ秒を超えて増加します。これを克服するためにEdgeCachingを構築しました。 エッジキャッシング エッジキャッシングを使用すると、REST応答は、CDNと同様に、世界中のエッジロケーションにキャッシュされます。エッジキャッシングが有効になっている場合、平均で5msのグローバルレイテンシが見られます。 10の異なるリージョンにあるクライアントからのレイテンシー数を記録するベンチマークアプリケーションを参照し
-
ActiveRecord列挙型を使用した簡単で読み取り可能な属性の作成
「保留中」、「承認済み」、または「フラグ付き」のいずれかの質問を想像してみてください。または、「自宅」、「オフィス」、「モバイル」、または「ファックス」の電話番号(1982年の場合)。 一部のモデルでは、この種のデータが必要です。 いくつかの異なる値のうちの1つのみを持つことができる属性。そして、その値のセットはほとんど変更されません。 これは、単純なRubyの場合、記号を使用するだけの状況です。 PhoneNumberTypeまたはQuestionStatusモデルとbelongs_toを作成できます これらの価値観を保持するための関係ですが、それだけの価値はないようです。それらをya