Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

ルビーの隠された宝石:弾丸

データベースは多くのアプリケーションの中心であり、データベースに問題があると、深刻なパフォーマンスの問題が発生する可能性があります。

ActiveRecordやMongoidなどのORMは、実装を抽象化し、コードをより高速に配信するのに役立ちますが、内部で実行されているクエリを確認するのを忘れることがあります。

弾丸の宝石は、いくつかのよく知られたデータベース関連の問題を特定するのに役立ちます:

  1. 「N+1クエリ」:アプリケーションがクエリを実行してリストの各アイテムを読み込むとき
  2. 「UnusedEagerLoading」:アプリケーションがデータを読み込むとき。通常はN + 1クエリを回避するためですが、データは使用しません。
  3. 「MissingCounterCache」:アプリケーションがカウントクエリを実行して、関連するアイテムの数を取得する必要がある場合

この投稿では、次のことを示します。

  • bulletの設定方法 Rubyプロジェクトの宝石
  • 前述の各問題の例
  • どのようにbullet それぞれを検出します
  • 各問題を修正する方法、および
  • bulletを統合する方法 AppSignalを使用します。

この投稿のために作成したプロジェクトの例をいくつか使用します。

RubyプロジェクトでBulletを構成する方法

まず、gemをGemfileに追加します 。

指定されたすべての環境に追加したり、有効または無効にしたり、それぞれに異なるアプローチを使用したりできます。

gem 'bullet'

次に、それを構成する必要があります。

Railsプロジェクトに参加している場合は、次のコマンドを実行して構成コードを自動的に生成できます。

bundle exec rails g bullet:install

Rails以外のプロジェクトに参加している場合は、手動で追加できます。たとえば、spec_helper.rbに次のコードを追加します。 アプリケーションのコードをロードした後:

Bullet.enable        = true
Bullet.bullet_logger = true
Bullet.raise         = true

そして、アプリケーションのコードをロードした後、メインファイルに次のコードを追加します。

Bullet.enable = true

この投稿では、構成の詳細を共有します。それらすべてを表示したい場合は、箇条書きのREADMEページにアクセスしてください。

テストでの箇条書きの使用

以前に提案された構成では、Bulletはテストで実行された不正なクエリを検出し、それらの例外を発生させます。

それでは、いくつかの例を見てみましょう。

N+1クエリの検出

与えられたindex 次のようなアクション:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

そしてこのような見方:

# app/views/posts/index.html.erb
 
<h1>Posts</h1>
 
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Comments</th>
    </tr>
  </thead>
 
  <tbody>
    <% @posts.each do |post| %>
    <tr>
      <td><%= post.name %></td>
      <td><%= post.comments.map(&:name) %></td>
    </tr>
    <% end %>
  </tbody>
</table>

bullet たとえば、次のようなリクエストスペックを使用して、ビューとコントローラーからコードを実行する統合テストを実行すると、「N+1」の検出エラーが発生します。

# spec/requests/posts_request_spec.rb
require 'rails_helper'
 
RSpec.describe "Posts", type: :request do
  describe "GET /index" do
    it 'lists all posts' do
      post1 = Post.create!
      post2 = Post.create!
 
      get '/posts'
 
      expect(response.status).to eq(200)
    end
  end
end

この場合、この例外が発生します:

Failures:

  1) Posts GET /index lists all posts
     Failure/Error: get '/posts'

     Bullet::Notification::UnoptimizedQueryError:
       user: fabioperrella
       GET /posts
       USE eager loading detected
         Post => [:comments]
         Add to your query: .includes([:comments])
       Call stack
         /Users/fabioperrella/projects/bullet-test/app/views/posts/index.html.erb:17:in `map'
         ...
     # ./spec/requests/posts_controller_spec.rb:9:in `block (3 levels) in <top (required)>'

これは、ビューが1つのクエリを実行して、post.comments.map(&:name)の各コメント名を読み込むために発生します。 :

Processing by PostsController#index as HTML
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
  Comment Load (0.0ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:17:in `map'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]

これを修正するには、エラーメッセージの指示に従い、.includes([:comments])を追加します。 クエリへ:

-@posts = Post.all
+@posts = Post.all.includes([:comments])

これにより、ActiveRecordは1つのクエリだけですべてのコメントをロードするように指示されます。

Processing by PostsController#index as HTML
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
  Comment Load (0.0ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (?, ?)  [["post_id", 1], ["post_id", 2]]
  ↳ app/views/posts/index.html.erb:14

ただし、bullet コントローラテストはデフォルトでビューをレンダリングしないため、次のようなコントローラテストで例外が発生することはありません。そのため、N+1クエリはトリガーされません。

注:Rails 5以降、コントローラーテストは推奨されていません:

# spec/controllers/posts_controller_spec.rb
require 'rails_helper'
 
RSpec.describe PostsController do
  describe 'GET index' do
    it 'lists all posts' do
      post1 = Post.create!
      post2 = Post.create!
 
      get :index
 
      expect(response.status).to eq(200)
    end
  end
end

Bulletが「N+1」を検出しないテストの別の例は、ビューテストです。この場合、データベースでN+1クエリを実行しないためです。

# spec/views/posts/index.html.erb_spec.rb
require 'rails_helper'
 
describe "posts/index.html.erb" do
  it 'lists all posts' do
    post1 = Post.create!(name: 'post1')
    post2 = Post.create!(name: 'post2')
 
    assign(:posts, [post1, post2])
 
    render
 
    expect(rendered).to include('post1')
    expect(rendered).to include('post2')
  end
end

テストでN+1を検出する可能性を高めるためのヒント

コントローラのアクションごとに少なくとも1つのリクエスト仕様を作成することをお勧めします。これは、正しいHTTPステータスが返されるかどうかをテストしてから、bullet これらのビューをレンダリングするときにクエリを監視します。

未使用の熱心な読み込みの検出

次のbasic_indexが与えられます アクション:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def basic_index
    @posts = Post.all.includes(:comments)
  end
end

そして、次のbasic_index ビュー:

# app/views/posts/basic_index.html.erb
 
<h1>Posts</h1>
 
<table>
  <thead>
    <tr>
      <th>Name</th>
    </tr>
  </thead>
 
  <tbody>
    <% @posts.each do |post| %>
    <tr>
      <td><%= post.name %></td>
    </tr>
    <% end %>
  </tbody>
</table>

次のテストを実行する場合:

# spec/requests/posts_request_spec.rb
require 'rails_helper'
 
RSpec.describe "Posts", type: :request do
  describe "GET /basic_index" do
    it 'lists all posts' do
      post1 = Post.create!
      post2 = Post.create!
 
      get '/posts/basic_index'
 
      expect(response.status).to eq(200)
    end
  end
end

Bulletは次のエラーを発生させます:

  1) Posts GET /basic_index lists all posts
     Failure/Error: get '/posts/basic_index'

     Bullet::Notification::UnoptimizedQueryError:
       user: fabioperrella
       GET /posts/basic_index
       AVOID eager loading detected
         Post => [:comments]
         Remove from your query: .includes([:comments])
       Call stack
         /Users/fabioperrella/projects/bullet-test/spec/requests/posts_request_spec.rb:20:in `block (3 levels) in <top (required)>'

これは、このビューのコメントのリストをロードする必要がないために発生します。

この問題を解決するには、上記のエラーの指示に従って、クエリ.includes([:comments])を削除します。 :

-@posts = Post.all.includes(:comments)
+@posts = Post.all

render_viewsを使用せずに、コントローラーテストのみを実行しても、同じエラーは発生しないことは言うまでもありません。 、前に示したように。

欠落しているカウンターキャッシュの検出

このようなコントローラーが与えられた場合:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index_with_counter
    @posts = Post.all
  end
end

そしてこのような見方:

# app/views/posts/index_with_counter.html.erb
 
<h1>Posts</h1>
 
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Number of comments</th>
    </tr>
  </thead>
 
  <tbody>
    <% @posts.each do |post| %>
    <tr>
      <td><%= post.name %></td>
      <td><%= post.comments.size %></td>
    </tr>
    <% end %>
  </tbody>
</table>

次のリクエスト仕様を実行した場合:

describe "GET /index_with_counter" do
  it 'lists all posts' do
    post1 = Post.create!
    post2 = Post.create!
 
    get '/posts/index_with_counter'
 
    expect(response.status).to eq(200)
  end
end

bullet 次のエラーが発生します:

1) Posts GET /index_with_counter lists all posts
  Failure/Error: get '/posts/index_with_counter'

  Bullet::Notification::UnoptimizedQueryError:
    user: fabioperrella
    GET /posts/index_with_counter
    Need Counter Cache
      Post => [:comments]
  # ./spec/requests/posts_request_spec.rb:31:in `block (3 levels) in <top (required)>'

これは、このビューがpost.comments.size内のコメントの数をカウントするために1つのクエリを実行しているために発生します 投稿ごとに。

Processing by PostsController#index_with_counter as HTML
  ↳ app/views/posts/index_with_counter.html.erb:14
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index_with_counter.html.erb:14
   (0.4ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index_with_counter.html.erb:17
   (0.1ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]

これを修正するために、カウンターキャッシュを作成できます。これは、特に本番データベースにデータがある場合は、少し複雑になる可能性があります。

カウンターキャッシュは、テーブルに追加できる列であり、関連するモデルを挿入および削除すると、ActiveRecordが自動的に更新します。この投稿で詳細があります。カウンターキャッシュを作成して同期する方法を知るためにそれを読むことをお勧めします。

開発でのBulletの使用

テストカバレッジが低い場合など、テストで前述の問題が検出されない場合があるため、bulletを有効にすることができます。 異なるアプローチを使用する他の環境で。

開発環境では、次の構成を有効にできます。

Bullet.alert         = true

次に、ブラウザに次のようなアラートが表示されます:

Bullet.add_footer    = true

エラーのあるページにフッターが追加されます:

ブラウザのコンソールにエラーを記録できるようにすることもできます:

Bullet.console    = true

次のようなエラーが追加されます:

Appsignalを使用したステージングでのBulletの使用

ステージング 環境では、これらのエラーメッセージがエンドユーザーに表示されることは望ましくありませんが、アプリケーションで前述の問題の1つが発生し始めているかどうかを知ることは素晴らしいことです。

同時に、bullet アプリケーションのパフォーマンスが低下し、メモリ消費量が増える可能性があるため、ステージングで一時的にのみ有効にすることをお勧めします。 、ただし、本番では有効にしないでください 。

ステージングを想定 環境は本番と同じ構成ファイルを使用しています 環境は、それらの違いを減らすための良い習慣です。環境変数を使用して、bulletを有効または無効にできます。 次のように:

# config/environments/production.rb
config.after_initialize do
  Bullet.enabled   = ENV.fetch('BULLET_ENABLED', false)
  Bullet.appsignal = true
end

Bulletがステージング環境で検出した問題に関する通知を受信するには、AppSignalを使用してそれらの通知をエラーとして報告できます。 appsignalが必要です プロジェクトにインストールおよび構成されたgem。詳細については、Rubygemドキュメントをご覧ください。

次に、bulletで問題が検出された場合 、次のようなエラーインシデントが発生します:

このエラーは、bulletから抽出されたuniform_notifiergemによって発生します。 。

残念ながら、エラーメッセージには十分な情報が表示されませんが、これを改善するためにプルリクエストを送信しました!

結論

bullet gemは、アプリケーションのパフォーマンスを低下させる問題を検出するのに役立つ優れたツールです。

前述のように、本番環境に移行する前にこれらの問題を検出する可能性を高めるために、適切なテストカバレッジを維持するようにしてください。

追加のヒントとして、データベースに関連するパフォーマンスの問題からさらに保護したい場合は、適切なインデックスを使用していないクエリを検出するのに役立つwt-activerecord-index-spygemを確認してください。

>

P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。


  1. Ruby2.6の9つの新機能

    Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,

  2. 知っておくべきWindows 11のヒントと隠された宝石

    Windows 11 は、互換性のあるデバイスの無料アップグレードとして利用でき、多くの新機能と改善が含まれています。新しく再設計されたスタート メニュー タスクバー、Andriod アプリをサポートする改良された Microsoft ストア、統合された Microsoft チーム、スナップ レイアウト、ウィジェットなどがあります。しかし、レドモンドの巨人によって公式に発表された大きなニュースと機能に加えて、Windows 11 には多くの小さな変更が含まれており、一見すると明らかではないかもしれません。この投稿では、Windows 11 の非表示の機能をいくつか紹介しました。 について知って