Computer >> Máy Tính >  >> Lập trình >> Ruby

Bộ nhớ đệm với bộ nhớ đệm của ActiveRecord

Thay vì đếm các bản ghi được liên kết trong cơ sở dữ liệu mỗi khi tải trang, tính năng bộ nhớ đệm bộ đếm của ActiveRecord cho phép lưu trữ bộ đếm và cập nhật nó mỗi khi một đối tượng được liên kết được tạo hoặc xóa. Trong tập này của AppSignal Academy, chúng ta sẽ tìm hiểu tất cả về bộ đếm bộ nhớ đệm trong ActiveRecord.

Hãy lấy ví dụ cổ điển về một blog với các bài báo và phản hồi. Mỗi bài viết có thể có câu trả lời và chúng tôi muốn hiển thị số lượng câu trả lời bên cạnh tiêu đề của mỗi bài viết trên trang chỉ mục của blog để thể hiện mức độ phổ biến của nó.

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

Chúng tôi không phải tải trước câu trả lời, vì chúng tôi không hiển thị dữ liệu của họ trên trang chỉ mục. Chúng tôi đang trưng bày một bộ đếm, vì vậy chúng tôi chỉ quan tâm đến số lượng phản hồi cho mỗi bài viết. Bộ điều khiển tìm tất cả các bài báo và đặt chúng vào @articles biến cho chế độ xem để sử dụng.

<!-- 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 %>

Chế độ xem lặp lại trên mỗi bài viết và hiển thị tiêu đề, mô tả và số lượng phản hồi mà nó nhận được. Bởi vì chúng tôi gọi article.responses.size trong chế độ xem, ActiveRecord biết nó cần đếm liên kết thay vì tải toàn bộ bản ghi cho mỗi phản hồi.

Mẹo :Mặc dù #count nghe có vẻ như là lựa chọn trực quan hơn để đếm số lượng phản hồi, ví dụ này sử dụng #size , dưới dạng #count sẽ luôn thực hiện COUNT truy vấn, trong khi #size sẽ bỏ qua truy vấn nếu các câu trả lời đã được tải.

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)

Yêu cầu chỉ mục của blog dẫn đến N + 1 truy vấn, vì ActiveRecord tải lười biếng số lượng phản hồi cho mỗi bài viết trong một truy vấn riêng biệt.

Sử dụng COUNT() từ truy vấn

Để tránh chạy thêm một truy vấn cho mỗi bài viết, chúng ta có thể nối các bài viết và bảng câu trả lời lại với nhau để đếm các câu trả lời liên quan trong một truy vấn.

# 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

Trong ví dụ này, chúng tôi kết hợp các câu trả lời trong truy vấn bài viết và chọn COUNT("responses.id") để đếm số lượng câu trả lời. Chúng tôi sẽ nhóm theo ID sản phẩm để tính số phản hồi cho mỗi bài viết. Trong chế độ xem, chúng tôi sẽ cần sử dụng responses_count thay vì gọi size trên hiệp hội câu trả lời.

Giải pháp này ngăn chặn các truy vấn bổ sung bằng cách làm cho truy vấn đầu tiên chậm hơn và phức tạp hơn. Mặc dù đây là bước đầu tiên tốt trong việc tối ưu hóa hiệu suất của trang này, nhưng chúng tôi có thể tiến thêm một bước nữa và lưu vào bộ nhớ đệm để chúng tôi không cần đếm từng phản hồi trên mỗi lần xem trang.

Bộ nhớ đệm của bộ đếm

Vì các bài viết trên blog (hy vọng) được đọc thường xuyên hơn chúng được cập nhật, một bộ nhớ đệm bộ đếm là một cách tối ưu hóa tốt để làm cho việc truy vấn trang này nhanh hơn và đơn giản hơn.

Thay vì đếm số lượng phản hồi mỗi khi bài viết được hiển thị, bộ đệm bộ đếm giữ một bộ đếm phản hồi riêng biệt được lưu trữ trong mỗi hàng cơ sở dữ liệu của bài viết. Bộ đếm cập nhật bất cứ khi nào một phản hồi được thêm vào hoặc xóa bỏ.

Điều này cho phép chỉ mục bài viết hiển thị với một truy vấn cơ sở dữ liệu mà không cần kết hợp các phản hồi trong truy vấn. Để thiết lập, hãy lật công tắc trong belongs_to bằng cách đặt counter_cache tùy chọn.

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

Điều này yêu cầu một trường cho Article mô hình có tên responses_count . counter_cache đảm bảo số trong trường đó tự động cập nhật bất cứ khi nào phản hồi được thêm vào hoặc xóa.

Mẹo :Tên trường có thể được ghi đè bằng cách sử dụng ký hiệu thay vì true làm giá trị cho counter_cache tùy chọn.

Chúng tôi tạo một cột mới trong cơ sở dữ liệu của mình để lưu trữ số lượng.

$ 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) =============

Bởi vì số lượng câu trả lời hiện đã được lưu trong bộ nhớ cache trong bảng bài viết, chúng tôi không cần kết hợp các câu trả lời trong truy vấn bài viết. Chúng tôi sẽ sử dụng Article.all để tìm nạp tất cả các bài báo trong bộ điều khiển.

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

Chúng tôi không cần thay đổi chế độ xem, vì Rails hiểu là sử dụng bộ đệm ẩn bộ đếm cho #size phương pháp.

<!-- 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 %>

Yêu cầu lại chỉ mục của chúng tôi, chúng tôi có thể thấy một truy vấn đang được thực thi. Bởi vì mỗi bài viết biết số lượng câu trả lời của nó, nó không cần phải truy vấn bảng câu trả lời.

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)

Bộ nhớ đệm bộ đếm cho các liên kết trong phạm vi

Lệnh gọi lại bộ đệm bộ đếm của ActiveRecord chỉ kích hoạt khi tạo hoặc hủy bản ghi, vì vậy việc thêm bộ đệm bộ đếm vào một liên kết có phạm vi sẽ không hoạt động. Đối với các trường hợp nâng cao, chẳng hạn như chỉ đếm số lượng phản hồi * đã xuất bản *, hãy xem đá quý counter_culture.

Đang điền vào bộ nhớ đệm của bộ đếm

Đối với các bài viết có trước bộ nhớ cache của bộ đếm, bộ đếm sẽ không đồng bộ, vì nó là 0 theo mặc định. Chúng tôi có thể "đặt lại" bộ đếm cho một đối tượng bằng cách sử dụng .reset_counters phương thức trên đó và chuyển ID của đối tượng và mối quan hệ mà bộ đếm sẽ được cập nhật.

Article.reset_counters(article.id, :responses)

Để đảm bảo điều này chạy ở chế độ sản xuất khi chúng tôi triển khai, chúng tôi sẽ đưa nó vào một quá trình di chuyển chạy trực tiếp sau khi thêm cột trong lần di chuyển cuối cùng.

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

Trong quá trình di chuyển, chúng tôi sẽ gọi Article.reset_counters cho mỗi bài viết, chuyển ID của bài viết và :responses như tên của hiệp hội.

# 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

Quá trình di chuyển này cập nhật số lượng cho tất cả các bài viết trong cơ sở dữ liệu bao gồm cả những bài viết tồn tại trước bộ nhớ đệm bộ đếm.

Gọi lại

Vì bộ nhớ đệm của bộ đếm sử dụng lệnh gọi lại để cập nhật bộ đếm, các phương thức thực thi trực tiếp các lệnh SQL (như khi sử dụng #delete thay vì #destroy ) sẽ không cập nhật bộ đếm.

Trong các tình huống mà điều đó xảy ra vì một lý do nào đó, có thể hợp lý khi thêm một công việc Rake hoặc một công việc nền để giữ cho số lượng được đồng bộ hóa theo định kỳ.

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

Bộ đếm trong bộ nhớ cache

Việc ngăn chặn N + 1 truy vấn bằng cách đếm các đối tượng được liên kết trong truy vấn có thể hữu ích, nhưng bộ đếm trong bộ nhớ đệm là một cách thậm chí còn nhanh hơn để hiển thị bộ đếm cho hầu hết các ứng dụng. Các bộ đếm được lưu trong bộ nhớ cache tích hợp của ActiveRecord có thể giúp ích rất nhiều và các tùy chọn như counter_culture có thể được sử dụng cho các yêu cầu phức tạp hơn.

Bạn có bất kỳ câu hỏi nào về bộ nhớ đệm bộ đếm của ActiveRecord? Vui lòng cho chúng tôi biết tại @AppSignal. Tất nhiên, chúng tôi muốn biết bạn thích bài viết này như thế nào hoặc nếu bạn có chủ đề khác mà bạn muốn biết thêm.