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

เพิ่มประสิทธิภาพ ActiveRecord: ใช้ Caching กำจัด Redundant Database Queries

วิธีอธิบาย Caching แบบทั่วไปคือการเก็บ Result ของ Code บางอย่างไว้ เพื่อ Retrieve เร็วขึ้นภายหลัง บางกรณีหมายถึง Store Computed Value กัน Recompute ภายหลัง แต่เรายัง Cache Data ได้โดยแค่เก็บใน Memory โดยไม่ Compute อะไร เพื่อกันการอ่าน Hard Drive หรือ Network Request

รูปแบบหลังนี้เกี่ยวข้องกับ ActiveRecord โดยเฉพาะ ที่ Database มักรันบน Separate Server ดังนั้น Requests ทั้งหมดมี Network-traffic Overhead ไม่รวม Load บน Database Server ตอน Query รันอีกครั้ง

โชคดีสำหรับ Rails Developers ActiveRecord เองจัดการมากมายให้เราแล้ว อาจโดยเราไม่ Conscious นี่ดีสำหรับ Productivity แต่บางครั้งสำคัญที่จะรู้ว่าอะไรถูก Cached ข้างหลัง เช่น ตรวจสอบว่า Value ถูกเปลี่ยนโดย Process อื่น หรือ ต้องการ Up-to-date Value อย่างแน่นอน ใน Cases เหล่านี้ ActiveRecord ให้ 'Escape Hatches' สองสามตัว Force Uncached Read

ActiveRecord's Lazy Evaluation

Lazy Evaluation ของ ActiveRecord ไม่ใช่ Caching ตรงๆ แต่เราจะเจอใน Code Examples ต่อไป จะ Overview สั้นๆ ตอน Construct ActiveRecord Query หลายกรณี Code ไม่ Issue Immediate Call ไป Database ทันที นี่คือเหตุที่เรา Chain .where หลายตัวได้โดยไม่ต้อง Hit Database ทุกครั้ง:


@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...

มี Exceptions เช่น ใช้ .find, .find_by, .pluck, .to_a, .first ไม่ Chain Additional Clauses ได้ ใน Examples ด้านล่างใช้ .to_a Force DB Call แบบง่าย

ถ้าลองใน Rails Console ต้องปิด 'echo' Mode ไม่งั้น Console (irb หรือ pry) เรียก .inspect ตอน Enter บังคับ DB Query ปิด Echo Mode:


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

ActiveRecord Relations

ส่วนแรกของ Built-in Caching ของ ActiveRecord คือ Relations ตัวอย่าง Typical User-Posts Relationship:


# 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 Methods Query Related Records สมมติใช้ใน Controller และ View:


# 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| %>
  
  • <%= post.title %>
  • <% end %>

    Basic index Action Grab @user.posts คล้ายก่อนหน้า DB Query ยังไม่รัน Rails Render index View ซึ่ง Render Sidebar Sidebar เรียก @posts.each... ที่นี่ ActiveRecord ยิง DB Query ดึง Data

    กลับมาที่ index Template อีก @posts.each ครั้งที่สอง ครั้งนี้ No Database Call ActiveRecord Cache Posts ทั้งหมด ไม่ยิง DB อีก

    Escape Hatch

    มีครั้งที่อยาก Force ActiveRecord Fetch Associated Records อีกครั้ง อาจรู้ว่าถูกเปลี่ยนโดย Process อื่น (Background Job) อีกสถานการณ์ Automated Tests อยาก Latest Value ใน DB Validate ว่า Code Update ถูกต้อง

    2 วิธีทั่วไป ขึ้นอยู่กับ Situation วิธีcommon: เรียก .reload บน Association บอก ActiveRecord ว่าอยาก Ignore Cache 取 Latest จาก DB:

    
    @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
    

    อีกวิธี: Get New Instance ของ ActiveRecord Model (เช่น เรียก find อีกครั้ง):

    
    @user = User.find(1)
    @user.posts # DB Call
    @user.posts # Cached, no DB call
    @user = User.find(1) # @user ตอนนี้เป็น New Instance ของ User
    @user.posts # DB Call, no cache in this instance
    

    Caching Relationships ดี แต่เรามักมี .where(...) Queries ซับซ้อนกว่า Simple Relationship Lookups นี่คือที่ SQL Cache ของ ActiveRecord เข้ามา

    ActiveRecord's SQL Cache

    ActiveRecord เก็บ Internal Cache ของ Queries ที่รันแล้ว เพื่อ Speed Up Performance โปรดทราบ Cache นี้ผูกกับ Action เฉพาะ สร้างตอนเริ่ม Action ทำลายตอนจบ Action หมายถึงคุณเห็นได้ก็ต่อเมื่อ Query ซ้ำใน Controller Action เดียว มัน also หมายถึง Cache ไม่ได้ใช้ใน Rails Console Cache Hits แสดงใน Rails Log ด้วย CACHE ตัวอย่าง:

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

    Log Output:

    
    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'
    

    Peek Cache เนื้อหาได้โดย Print ActiveRecord::Base.connection.query_cache (หรือ .keys สำหรับ SQL Queries เฉพาะ)

    Escape Hatch

    อาจไม่มีเหตุผลมากที่ต้อง Bypass SQL Cache แต่ Force ได้ด้วย uncached Method บน ActiveRecord::Base:

    
    class PostsController < ApplicationController
      def index
        ...
        Post.all.to_a # to_a force DB query
        ...
        ActiveRecord::Base.uncached do
          Post.all.to_a # to_a force DB query
        end
    

    เพราะเป็น Method บน ActiveRecord::Base เรียกผ่าน Model Class ก็ได้ถ้า Readability ดีกว่า เช่น:

    
    Post.uncached do
      Post.all.to_a
    end
    

    Counter Cache

    Common ใน Web Apps อยาก Count Records ใน Relationship (User has X Posts, Team has Y Users) เพราะ Common ActiveRecord ให้ Auto Keep Counter Up-to-date กัน .count Calls ใช้ Database Resources มาก ใช้ได้ 2 Steps Enable: เพิ่ม counter_cache ใน Relationship บอก ActiveRecord Cache Count ให้:

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

    ต้อง Add Column ใหม่ใน User เก็บ Count ตัวอย่าง User.posts_count ส่ง Symbol ให้ counter_cache ระบุ Column Name ได้

    
    rails generate migration AddPostsCountToUsers posts_count:integer
    rails db:migrate
    

    Counters ตั้งเป็น 0 (Default) ถ้า App มี Posts อยู่แล้ว ต้อง Update ActiveRecord ให้ reset_counters Method จัดการ Details Pass IDs และบอก Counter ไหน Update:

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

    เช็คที่ใช้ Count นี้ เพราะ .count Bypass Counter รัน COUNT() SQL Query เสมอ ใช้ .size แทน รู้ว่าใช้ Counter Cache ถ้ามี Aside: ควร Default ใช้ .size ทุกที่ เพราะมันไม่ Reload Associations ถ้ามีอยู่แล้ว Save DB Trip ได้อีก

    สรุป

    ส่วนใหญ่ ActiveRecord Internal Caching "Just Works" ไม่คิดว่ามี Cases มากที่ต้อง Bypass แต่เช่นทุกอย่าง การรู้ว่า Under-the-hood เกิดอะไรขึ้น ช่วย Save Time และ Agony ตอนเจอ Situation ที่ต้องการ Out-of-the-ordinary

    Database ไม่ใช่ที่เดียวที่ Rails Behind-the-scenes Caching ให้ HTTP Spec มี Headers ส่งระหว่าง Client/Server กัน Re-send Data ที่ไม่เปลี่ยน ต่อไปใน Series Caching นี้ เราจะดู 304 (Not Modified) HTTP Status Code Rails Handle ให้ยังไง และ Tweak Handling ยังไง