Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tối Ưu ActiveRecord: Tận Dụng Caching Để Loại Bỏ Truy Vấn Database Dư Thừa

Một cách chung để mô tả caching là lưu trữ kết quả của một đoạn code để chúng ta có thể retrieve nó nhanh chóng sau. Trong một số trường hợp, nghĩa là lưu computed value để tránh recompute sau. Tuy nhiên, chúng ta cũng có thể cache data bằng cách đơn giản giữ nó trong memory, không thực hiện computation nào, để tránh phải đọc từ hard drive hoặc thực hiện network request.

Form sau này đặc biệt liên quan cho ActiveRecord, nơi database thường chạy trên server riêng. Do đó, tất cả request mang network-traffic overhead, không kể load đặt lên database server khi query được thực hiện lại.

May mắn thay, cho Rails developer, ActiveRecord bản thân đã handle rất nhiều điều này cho chúng ta, có lẽ mà chúng ta thậm chí không nhận ra. Điều này tốt cho productivity, nhưng đôi khi, quan trọng là biết gì đang được cached behind-the-scenes. Ví dụ, khi bạn biết (hoặc kỳ vọng) một value đang bị thay đổi bởi process khác, hoặc bạn absolutely must có giá trị up-to-date nhất. Trong cases như này, ActiveRecord cung cấp một couple 'escape hatches' để force uncached read của data.

Lazy Evaluation Của ActiveRecord

Lazy evaluation của ActiveRecord không phải caching per se, nhưng chúng ta sẽ gặp nó trong code examples sau, nên chúng ta sẽ cung cấp brief overview. Khi bạn construct ActiveRecord query, trong nhiều cases, code không issue immediate call đến database. Điều này cho phép chain multiple .where clauses mà không phải hit database mỗi lần:


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

Có một số exception. Ví dụ, khi dùng .find, .find_by, .pluck, .to_a, hoặc .first, không thể chain additional clauses. Trong hầu hết examples dưới, tôi sẽ dùng .to_a như cách đơn giản force DB call.

Lưu ý nếu bạn experiment với này trong Rails console, bạn sẽ cần tắt 'echo' mode. Nếu không, console (either irb or pry) gọi .inspect trên object khi bạn hit 'enter', which forces DB query. Để disable echo mode, bạn có thể dùng code sau:


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

ActiveRecord Relations

Phần đầu tiên của built-in caching của ActiveRecord chúng ta sẽ nhìn là relations. Ví dụ, chúng ta có 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

Điều này cho chúng ta handy user.posts và post.user methods để perform database query tìm related record(s). Giả sử chúng ta dùng chúng trong controller và 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| %>
    <li><%= post.title %></li>
  <% end %>

Chúng ta có basic index action grab @user.posts. Tương tự section trước, database query chưa chạy ở điểm này. Rails sau đó render index view, which, in turn, render sidebar. Sidebar gọi @posts.each ..., và tại điểm này, ActiveRecord fires off database query để lấy data.

Chúng ta sau đó quay lại phần còn lại của index template, nơi chúng ta có another @posts.each; tuy nhiên, lần này, không có database call. Điều xảy ra là ActiveRecord đang cache tất cả posts này cho chúng ta và không bother thử đọc từ database lại.

Escape Hatch

Có lúc chúng ta muốn force ActiveRecord fetch associated records lại; perhaps, chúng ta biết nó đang bị thay đổi bởi process khác (background job, ví dụ). Tình huống phổ biến khác là trong automated tests nơi chúng ta muốn lấy latest value trong database để validate code đã update đúng.

Có hai cách phổ biến để làm này, tùy situation. Tôi nghĩ cách phổ biến nhất là đơn giản gọi .reload trên association, which tells ActiveRecord rằng chúng ta muốn ignore whatever nó cached và get latest version từ database:


@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

Option khác là đơn giản get instance mới của ActiveRecord model (ví dụ, bằng cách gọi find lại):


@user = User.find(1)
@user.posts # DB Call
@user.posts # Cached, no DB call
@user = User.find(1) # @user bây giờ là instance mới của User
@user.posts # DB Call, no cache trong instance này

Caching relationship tốt, nhưng chúng ta thường end up với phức tạp .where(...) queries hơn simple relationship lookups. Đây là nơi ActiveRecord's SQL cache xuất hiện.

SQL Cache Của ActiveRecord

ActiveRecord giữ internal cache của queries nó đã perform để speed up performance. Lưu ý, tuy nhiên, cache này tied to particular action; nó được tạo tại start của action và destroyed tại end của action. Nghĩa là bạn chỉ thấy này nếu bạn performing same query twice trong một controller action. Nó cũng nghĩa cache không dùng trong Rails console. Cache hits hiển thị trong Rails log với CACHE. Ví dụ,


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

produces log output sau:


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'

Bạn thực sự có thể peek vào cache cho action bằng cách print ActiveRecord::Base.connection.query_cache (hoặc ActiveRecord::Base.connection.query_cache.keys cho chỉ SQL query).

Escape Hatch

Có lẽ không nhiều lý do bạn cần bypass SQL Cache, nhưng nevertheless, bạn có thể force ActiveRecord bypass SQL cache bằng cách dùng phương thức uncached trên 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
  end
end

Vì nó là method trên ActiveRecord::Base, bạn cũng có thể gọi nó via một model class nếu nó cải thiện readability; ví dụ,


Post.uncached do
  Post.all.to_a
end

Counter Cache

Phổ biến trong web application muốn count record trong relationship (ví dụ, user có X posts hoặc team account có Y users). Do phổ biến như vậy, ActiveRecord include cách tự động keep counter up-to-date để bạn không có bunch .count calls dùng database resources. Chỉ mất vài bước enable nó. Đầu tiên, chúng ta add counter_cache vào relationship để ActiveRecord biết cache count cho chúng ta:


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

Chúng ta cũng cần add column mới vào User, nơi count sẽ được lưu. Trong ví dụ, đây sẽ là User.posts_count. Bạn có thể pass symbol vào counter_cache để specify column name nếu cần.


rails generate migration AddPostsCountToUsers posts_count:integer
rails db:migrate

Counters bây giờ sẽ được set thành 0 (default). Nếu application đã có một số posts, bạn sẽ cần update chúng. ActiveRecord cung cấp phương thức reset_counters handle nitty-gritty details, vì vậy bạn chỉ cần pass IDs và nói counter nào update:


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

Cuối cùng, chúng ta phải check nơi count này đang được dùng. Vì gọi .count sẽ bypass counter và luôn chạy COUNT() SQL query. Thay vào đó, chúng ta có thể dùng .size, which knows dùng counter cache nếu nó tồn tại. As an aside, bạn có thể muốn default dùng .size everywhere, vì nó cũng không reload associations nếu chúng đã present, potentially saving trip đến database.

Kết Luận

Đa phần, internal caching của ActiveRecord "just works". Tôi không thể nói tôi thấy nhiều case cần bypass nó, nhưng như mọi thứ, biết gì diễn ra "under-the-hood" có thể save bạn thời gian và agonize khi bạn vấp vào situation yêu cầu something out of the ordinary.

Khoảng, database không phải nơi duy nhất Rails đang làm behind-the-scenes caching cho chúng ta. HTTP specification bao gồm headers có thể gửi giữa client và server để tránh re-send data chưa thay đổi. Trong bài tiếp theo trong series caching này, chúng ta sẽ nhìn vào 304 (Not Modified) HTTP status code, cách Rails handle nó cho bạn, và cách bạn có thể tweak handling này.