Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

วิธีที่ ActiveRecord ใช้แคชเพื่อหลีกเลี่ยงการเดินทางที่ไม่จำเป็นไปยังฐานข้อมูล

วิธีทั่วไปในการอธิบายการแคชคือการจัดเก็บผลลัพธ์ของโค้ดบางส่วน เพื่อให้เราสามารถเรียกค้นคืนในภายหลังได้อย่างรวดเร็ว ในบางกรณี นี่หมายถึงการจัดเก็บค่าที่คำนวณไว้เพื่อหลีกเลี่ยงความจำเป็นในการคำนวณใหม่ในภายหลัง อย่างไรก็ตาม เรายังสามารถแคชข้อมูลได้ด้วยการเก็บไว้ในหน่วยความจำโดยไม่ต้องทำการคำนวณใดๆ เพื่อหลีกเลี่ยงการอ่านจากฮาร์ดไดรฟ์หรือดำเนินการตามคำขอของเครือข่าย

แบบฟอร์มหลังนี้มีความเกี่ยวข้องโดยเฉพาะกับ ActiveRecord ซึ่งฐานข้อมูลมักจะทำงานบนเซิร์ฟเวอร์แยกต่างหาก ดังนั้น คำขอทั้งหมดจึงมีค่าใช้จ่ายในการเข้าชมเครือข่าย ไม่ต้องพูดถึงภาระที่วางไว้บนเซิร์ฟเวอร์ฐานข้อมูลเมื่อดำเนินการค้นหาอีกครั้ง

โชคดีสำหรับนักพัฒนา Rails ActiveRecord ได้จัดการเรื่องนี้หลายอย่างสำหรับเราแล้ว โดยที่เราไม่รู้ตัวด้วยซ้ำ สิ่งนี้ดีสำหรับประสิทธิภาพการทำงาน แต่บางครั้ง สิ่งสำคัญคือต้องรู้ว่าสิ่งใดถูกแคชไว้เบื้องหลัง ตัวอย่างเช่น เมื่อคุณทราบ (หรือคาดหวัง) ค่าจะถูกเปลี่ยนโดยกระบวนการอื่น หรือคุณต้องมีค่าที่เป็นปัจจุบันที่สุด ในกรณีเช่นนี้ ActiveRecord จะมี 'ช่องหลบหนี' สองสามช่องเพื่อบังคับให้อ่านข้อมูลที่ไม่ได้แคชไว้

การประเมินความขี้เกียจของ ActiveRecord

การประเมินแบบ Lazy ของ ActiveRecord ไม่ใช่การแคชต่อ se แต่เราจะพบมันในตัวอย่างโค้ดในภายหลัง ดังนั้นเราจะให้ภาพรวมโดยสังเขป เมื่อคุณสร้างแบบสอบถาม ActiveRecord ในหลายกรณี รหัสไม่เรียกฐานข้อมูลทันที นี่คือสิ่งที่ทำให้เราเชื่อมโยงหลาย .where โดยไม่ต้องกดฐานข้อมูลทุกครั้ง:

@posts = Post.where(published: true)
# no DB hit yet
@posts = @posts.where(publied_at: Date.today)
# still nothing
@posts.count
# SELECT COUNT(*) FROM "posts" WHERE...

มีข้อยกเว้นบางประการสำหรับเรื่องนี้ ตัวอย่างเช่น เมื่อใช้ .find , .find_by , .pluck , .to_a , หรือ .first เป็นไปไม่ได้ที่จะโยงประโยคเพิ่มเติม ในตัวอย่างส่วนใหญ่ด้านล่าง ฉันจะใช้ .to_a เป็นวิธีง่ายๆ ในการบังคับเรียก DB

โปรดทราบว่าหากคุณกำลังทดลองสิ่งนี้ในคอนโซล Rails คุณจะต้องปิดโหมด 'echo' มิฉะนั้น คอนโซล (ทั้ง irb หรือ pry) จะเรียก .inspect บนวัตถุเมื่อคุณกด 'Enter' ซึ่งบังคับการสืบค้น DB หากต้องการปิดใช้งานโหมดสะท้อน คุณสามารถใช้รหัสต่อไปนี้:

conf.echo = false # for irb
pry_instance.config.print = proc {} # for pry

ความสัมพันธ์ของ ActiveRecord

ส่วนแรกของการแคชในตัวของ ActiveRecord เราจะดูที่ความสัมพันธ์ ตัวอย่างเช่น เรามี User-Posts . ทั่วไป ความสัมพันธ์:

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

สิ่งนี้ทำให้เรามี user.posts . ที่มีประโยชน์ และ post.user วิธีการดำเนินการสอบถามฐานข้อมูลเพื่อค้นหาเรกคอร์ดที่เกี่ยวข้อง สมมติว่าเรากำลังใช้สิ่งเหล่านี้ในคอนโทรลเลอร์และดู:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @user = User.find(params[:user_id])
    @posts = @user.posts
  end
...

# app/views/posts/index.html.erb
...
<%= render 'shared/sidebar' %>
<% @posts.each do |post| %>
  <%= render post %>
<% end %>

# app/views/shared/_sidebar.html.erb
...
<% @posts.each do |post| %>
  <li><%= post.title %></li>
<% end %>

เรามี index basic พื้นฐาน การกระทำที่คว้า @user.posts . คล้ายกับส่วนก่อนหน้า ไม่มีการเรียกใช้แบบสอบถามฐานข้อมูล ณ จุดนี้ Rails จะแสดง index . ของเรา มุมมองซึ่งในทางกลับกันทำให้แถบด้านข้าง แถบด้านข้างเรียก @posts.each ... และ ณ จุดนี้ ActiveRecord จะปิดการสืบค้นฐานข้อมูลเพื่อรับข้อมูล

จากนั้นเรากลับไปที่ index . ที่เหลือของเรา เทมเพลตที่เรามี อื่น @posts.each; อย่างไรก็ตาม คราวนี้ ไม่มีการเรียกฐานข้อมูล สิ่งที่เกิดขึ้นคือ ActiveRecord กำลังแคชโพสต์ทั้งหมดเหล่านี้ให้เราและไม่ต้องพยายามอ่านจากฐานข้อมูลอีก

Escape-hatch

มีบางครั้งที่เราอาจต้องการบังคับให้ ActiveRecord ดึงข้อมูลระเบียนที่เกี่ยวข้องอีกครั้ง บางที เรารู้ว่ากระบวนการอื่นกำลังเปลี่ยนแปลงไป (เช่น งานเบื้องหลัง) สถานการณ์ทั่วไปอีกประการหนึ่งคือการทดสอบอัตโนมัติซึ่งเราต้องการรับค่าล่าสุดในฐานข้อมูลเพื่อตรวจสอบว่าโค้ดอัปเดตอย่างถูกต้อง

มีสองวิธีในการทำเช่นนี้ ขึ้นอยู่กับสถานการณ์ ฉันคิดว่าวิธีที่พบบ่อยที่สุดคือการเรียก .reload ในการเชื่อมโยงซึ่งบอก ActiveRecord ว่าเราต้องการละเว้นสิ่งที่แคชไว้และรับเวอร์ชันล่าสุดจากฐานข้อมูล:

@user = User.find(1)
@user.posts # DB Call
@user.posts # Cached, no DB call
@user.posts.reload # DB call
@user.posts # Cached new version, no DB call

อีกทางเลือกหนึ่งคือเพียงแค่รับอินสแตนซ์ใหม่ของโมเดล ActiveRecord (เช่น โดยการเรียก find อีกครั้ง):

@user = User.find(1)
@user.posts # DB Call
@user.posts # Cached, no DB call
@user = User.find(1) # @user is now a new instance of User
@user.posts # DB Call, no cache in this instance

การแคชความสัมพันธ์เป็นสิ่งที่ดี แต่เรามักจบลงด้วย .where(...) . ที่ซับซ้อน แบบสอบถามนอกเหนือจากการค้นหาความสัมพันธ์แบบธรรมดา นี่คือที่มาของแคช SQL ของ ActiveRecord

แคช SQL ของ ActiveRecord

ActiveRecord เก็บแคชภายในของการสืบค้นที่ดำเนินการเพื่อเร่งประสิทธิภาพ อย่างไรก็ตาม โปรดทราบว่าแคชนี้เชื่อมโยงกับการดำเนินการเฉพาะ มันถูกสร้างขึ้นเมื่อเริ่มการกระทำและถูกทำลายเมื่อสิ้นสุดการกระทำ ซึ่งหมายความว่าคุณจะเห็นสิ่งนี้ก็ต่อเมื่อคุณดำเนินการสืบค้นข้อมูลเดียวกันสองครั้งภายในการกระทำของตัวควบคุมเดียว มัน ด้วย หมายถึงแคชไม่ได้ใช้ในคอนโซล Rails จำนวนแคชจะแสดงในบันทึก Rails ด้วย CACHE . ตัวอย่างเช่น

class PostsController < ApplicationController
  def index
    ...
    Post.all.to_a # to_a to force DB query

    ...
    Post.all.to_a # to_a to force DB query

สร้างเอาต์พุตบันทึกต่อไปนี้:

  Post Load (2.1ms)  SELECT "posts".* FROM "posts"
  ↳ app/controllers/posts_controller.rb:11:in `index'
  CACHE Post Load (0.0ms)  SELECT "posts".* FROM "posts"
  ↳ app/controllers/posts_controller.rb:13:in `index'

คุณสามารถดูสิ่งที่อยู่ภายในแคชสำหรับการดำเนินการได้โดยการพิมพ์ ActiveRecord::Base.connection.query_cache (หรือ ActiveRecord::Base.connection.query_cache.keys สำหรับการสืบค้น SQL เท่านั้น)

Escape-hatch

อาจมีสาเหตุไม่มากที่คุณจะต้องเลี่ยงผ่านแคช SQL แต่อย่างไรก็ตาม คุณสามารถบังคับให้ ActiveRecord ข้ามแคชของ SQL ได้โดยใช้ uncached วิธีการใน ActiveRecord::Base :

class PostsController < ApplicationController
  def index
    ...
    Post.all.to_a # to_a to force DB query

    ...
    ActiveRecord::Base.uncached do
      Post.all.to_a # to_a to force DB query
    end

เนื่องจากเป็นวิธีการใน ActiveRecord::Base คุณยังสามารถเรียกมันผ่านหนึ่งในคลาสโมเดลของคุณถ้ามันช่วยให้อ่านง่ายขึ้น ตัวอย่างเช่น

  Post.uncached do
    Post.all.to_a
  end

แคชตัวนับ

เป็นเรื่องปกติในเว็บแอปพลิเคชันที่ต้องการนับระเบียนในความสัมพันธ์ (เช่น ผู้ใช้มีโพสต์ X หรือบัญชีทีมมีผู้ใช้ Y) เนื่องจากเป็นเรื่องปกติธรรมดา ActiveRecord จึงมีวิธีการอัปเดตตัวนับโดยอัตโนมัติเพื่อให้คุณไม่มี .count จำนวนมาก โทรโดยใช้ทรัพยากรฐานข้อมูล ใช้เวลาเพียงไม่กี่ขั้นตอนในการเปิดใช้งาน ขั้นแรก เราเพิ่ม counter_cache กับความสัมพันธ์เพื่อให้ ActiveRecord รู้ว่าจะแคชการนับให้เรา:

class Post < ApplicationRecord
  belongs_to :user, counter_cache: true
end

เรายังต้องเพิ่มคอลัมน์ใหม่ใน User ที่ซึ่งการนับจะถูกเก็บไว้ ในตัวอย่างของเรา นี่จะเป็น User.posts_count . คุณสามารถส่งสัญลักษณ์ไปที่ counter_cache เพื่อระบุชื่อคอลัมน์หากต้องการ

rails generate migration AddPostsCountToUsers posts_count:integer
rails db:migrate

ตัวนับจะถูกตั้งค่าเป็น 0 (ค่าเริ่มต้น) หากแอปพลิเคชันของคุณมีบางโพสต์อยู่แล้ว คุณจะต้องอัปเดต ActiveRecord ให้ reset_counters วิธีจัดการกับรายละเอียดที่สำคัญ ดังนั้นคุณเพียงแค่ต้องส่ง ID และบอกว่าตัวนับใดที่จะอัปเดต:

User.all.each do |user|
  User.reset_counters(user.id, :posts)
end

สุดท้ายเราต้องตรวจสอบสถานที่ที่ใช้การนับนี้ นี่เป็นเพราะการโทร .count จะข้ามตัวนับและจะเรียกใช้ COUNT() . เสมอ แบบสอบถาม SQL แต่เราสามารถใช้ .size . แทน ซึ่งรู้ว่าจะใช้แคชตัวนับถ้ามีอยู่ นอกเหนือจากนั้น คุณอาจต้องการเริ่มต้นโดยใช้ .size ทุกที่ เนื่องจากจะไม่โหลดการเชื่อมโยงใหม่หากมีอยู่แล้ว ซึ่งอาจบันทึกการเดินทางไปยังฐานข้อมูล

บทสรุป

ส่วนใหญ่การแคชภายในของ ActiveRecord "ใช้งานได้" ฉันไม่สามารถพูดได้ว่าฉันได้เห็นหลายกรณีที่จำเป็นต้องข้ามมันไป แต่เช่นเดียวกับทุกสิ่ง การรู้ว่าเกิดอะไรขึ้น "ใต้กระโปรงรถ" สามารถช่วยประหยัดเวลาและความเจ็บปวดให้คุณได้บ้างเมื่อคุณสะดุดเข้ากับสถานการณ์ที่ต้องการบางสิ่งบางอย่าง ไม่ธรรมดา

แน่นอนว่าฐานข้อมูลไม่ใช่ที่เดียวที่ Rails ทำการแคชเบื้องหลังให้เรา ข้อกำหนด HTTP ประกอบด้วยส่วนหัวที่สามารถส่งระหว่างไคลเอนต์และเซิร์ฟเวอร์เพื่อหลีกเลี่ยงการส่งข้อมูลที่ไม่เปลี่ยนแปลงอีกครั้ง ในบทความถัดไปของชุดข้อมูลแคช เราจะมาดูที่ 304 (Not Modified) รหัสสถานะ HTTP วิธีที่ Rails จัดการให้คุณ และวิธีปรับแต่งการจัดการนี้