แทนที่จะนับเรคคอร์ดที่เกี่ยวข้องในฐานข้อมูลทุกครั้งที่โหลดเพจ คุณลักษณะการแคชตัวนับของ 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)
การขอดัชนีของบล็อกส่งผลให้เกิดการสืบค้น N+1 เนื่องจาก ActiveRecord ขี้เกียจโหลดจำนวนการตอบกลับสำหรับแต่ละบทความในแบบสอบถามแยกต่างหาก
การใช้ 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")
เพื่อนับจำนวนคำตอบ เราจะจัดกลุ่มตามรหัสผลิตภัณฑ์เพื่อนับคำตอบต่อบทความ ในมุมมอง เราจะต้องใช้ 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 แน่นอน เราอยากทราบว่าคุณชอบบทความนี้อย่างไร หรือหากคุณมีหัวข้ออื่นที่ต้องการทราบข้อมูลเพิ่มเติม