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.