Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

ActiveRecord가 캐싱을 사용하여 데이터베이스에 대한 불필요한 이동을 방지하는 방법

캐싱을 설명하는 일반적인 방법은 나중에 빠르게 검색할 수 있도록 일부 코드의 결과를 저장하는 것입니다. 어떤 경우에는 나중에 다시 계산할 필요가 없도록 계산된 값을 저장해야 합니다. 그러나 하드 드라이브에서 읽거나 네트워크 요청을 수행할 필요가 없도록 계산을 수행하지 않고 단순히 메모리에 보관하여 데이터를 캐시할 수도 있습니다.

이 후자의 형식은 데이터베이스가 종종 별도의 서버에서 실행되는 ActiveRecord와 특히 관련이 있습니다. 따라서 쿼리가 다시 수행될 때 데이터베이스 서버에 가해지는 로드는 말할 것도 없고 모든 요청에 ​​네트워크 트래픽 오버헤드가 발생합니다.

다행히도 Rails 개발자의 경우 ActiveRecord 자체가 이미 우리를 위해 많은 것을 처리합니다. 아마도 우리가 의식하지 못하는 사이일 것입니다. 이것은 생산성에 좋지만 때로는 배후에서 캐시되는 것이 무엇인지 아는 것이 중요합니다. 예를 들어, 값이 다른 프로세스에 의해 변경되고 있음을 알고 있거나 예상할 때 또는 가장 최신 값을 가지고 있어야 합니다. 이와 같은 경우 ActiveRecord는 캐시되지 않은 데이터 읽기를 강제하기 위해 몇 가지 '탈출 해치'를 제공합니다.

ActiveRecord의 지연 평가

ActiveRecord의 지연 평가는 그 자체로 캐싱이 아니지만 나중에 코드 예제에서 이를 접하게 되므로 간략한 개요를 제공할 것입니다. 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 콘솔에서 이것을 실험하는 경우 '에코' 모드를 꺼야 합니다. 그렇지 않으면 콘솔(irb 또는 pry)이 .inspect를 호출합니다. DB 쿼리를 강제 실행하는 'Enter' 키를 누르면 개체에 대해 설명합니다. 에코 모드를 비활성화하려면 다음 코드를 사용할 수 있습니다.

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이 있습니다. @user.posts를 잡는 작업 . 이전 섹션과 유사하게 이 시점에서 데이터베이스 쿼리가 실행되지 않았습니다. 그런 다음 Rails는 index를 렌더링합니다. 뷰는 차례로 사이드바를 렌더링합니다. 사이드바는 @posts.each ...를 호출합니다. , 그리고 이 시점에서 ActiveRecord는 데이터베이스 쿼리를 실행하여 데이터를 가져옵니다.

그런 다음 index의 나머지 부분으로 돌아갑니다. 다른이 있는 템플릿 @posts.each; 그러나 이번에는 데이터베이스 호출이 없습니다. 문제는 ActiveRecord가 우리를 위해 이 모든 게시물을 캐싱하고 데이터베이스에서 다시 읽기를 시도하는 것을 귀찮게 하지 않는다는 것입니다.

이스케이프 해치

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(...)로 끝납니다. 단순한 관계 조회 이상의 쿼리. ActiveRecord의 SQL 캐시가 들어오는 곳입니다.

ActiveRecord의 SQL 캐시

ActiveRecord는 성능 속도를 높이기 위해 수행한 쿼리의 내부 캐시를 유지합니다. 그러나 이 캐시는 특정 작업과 연결되어 있습니다. 작업 시작 시 생성되고 작업 종료 시 소멸됩니다. 즉, 하나의 컨트롤러 작업 내에서 동일한 쿼리를 두 번 수행하는 경우에만 이를 볼 수 있습니다. 역시 Rails 콘솔에서 캐시가 사용되지 않음을 의미합니다. 캐시 적중은 CACHE와 함께 Rails 로그에 표시됩니다. . 예를 들어,

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 쿼리에만 해당).

이스케이프 해치

SQL 캐시를 우회해야 하는 이유는 많지 않을 수 있지만 uncached를 사용하여 ActiveRecord가 SQL 캐시를 우회하도록 할 수 있습니다. 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가 이를 처리하는 방법 및 이 처리를 조정할 수 있는 방법.