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)

การขอดัชนีของบล็อกส่งผลให้เกิดการสืบค้น 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 แน่นอน เราอยากทราบว่าคุณชอบบทความนี้อย่างไร หรือหากคุณมีหัวข้ออื่นที่ต้องการทราบข้อมูลเพิ่มเติม