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를 호출하는 대신 응답 협회에서.

이 솔루션은 첫 번째 쿼리를 더 느리고 더 복잡하게 만들어 추가 쿼리를 방지합니다. 이것은 이 페이지의 성능을 최적화하는 좋은 첫 번째 단계이지만 한 단계 더 나아가 카운터를 캐시할 수 있으므로 모든 페이지 보기에서 각 응답을 계산할 필요가 없습니다.

카운터 캐시

블로그의 기사가 업데이트되는 것보다 더 자주 읽히기 때문에 카운터 캐시 이 페이지를 더 빠르고 간단하게 쿼리할 수 있는 좋은 최적화입니다.

기사가 표시될 때마다 응답 수를 계산하는 대신 카운터 캐시는 각 기사의 데이터베이스 행에 저장된 별도의 응답 카운터를 유지합니다. 응답이 추가되거나 제거될 때마다 카운터가 업데이트됩니다.

이렇게 하면 쿼리에서 응답을 조인할 필요 없이 기사 인덱스를 하나의 데이터베이스 쿼리로 렌더링할 수 있습니다. 설정하려면 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) =============

이제 응답 수가 기사 테이블에 캐시되므로 기사 쿼리에서 응답을 조인할 필요가 없습니다. 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 %>

인덱스를 다시 요청하면 하나의 쿼리가 실행되는 것을 볼 수 있습니다. 각 기사는 응답 수를 알고 있기 때문에 응답 테이블을 전혀 쿼리할 필요가 없습니다.

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_culture gem을 확인하세요.

카운터 캐시 채우기

카운터 캐시보다 앞선 기사의 경우 카운터는 기본적으로 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로 알려주십시오. 물론 이 기사가 어떻게 마음에 들었는지, 또는 더 알고 싶은 다른 주제가 있는지 알고 싶습니다.