Rubyの隠れた宝石:弾丸

24230 ワード

データベースは多くのアプリケーションの中心です、そして、それに関する問題があることは重大なパフォーマンス問題を引き起こすかもしれません.
ActiveRecordとMongoidのようなOrmsは抽象的な実装を助け、コードをより速く提供しますが、時々、我々はどのようなクエリがフードの下で実行されているかをチェックすることを忘れてしまいます.
The bullet GEMは、いくつかのよく知られているデータベース関連の問題を識別できます.
  • リストの各項目を読み込むクエリを実行するとき
  • “未使用の熱心な負荷”:アプリケーションを読み込むときは通常、n + 1クエリを避けるために、それを使用していません
  • 「欠落カウンタキャッシュ」:アプリケーションが関連項目の数を得るためにカウントクエリを実行する必要があるとき
  • このポストでは、次のように表示します.
  • 設定方法bullet RubyプロジェクトのGEM
  • 前に述べた各問題の例.
  • ハウbullet 検出します.
  • それぞれの問題を解決する方法
  • ハウツーとスタイルbullet を指定します.
  • 私はいくつかの例を使用しますa project that I created for this post .

    Rubyプロジェクトでの弾丸の設定方法


    まず、宝石を追加する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
    
    私はこのポストの構成の詳細を共有するつもりです.あなたがそれらをすべて見たいならばbullet's README page .

    テストで弾丸を使う


    以前に提案された設定で、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クエリはトリガされません.
    注意:controller tests are discouraged 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
    
    弾丸が「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つの要求仕様を作成することを勧めます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)>'
    
    これは、このビューが1クエリを実行しているため、post.comments.size 各ポスト.
    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が関連するモデルを挿入して削除するときに自動的に更新されるテーブルに追加できる列です.詳細はthis post . 私はどのように作成し、カウンタキャッシュの同期を知るためにそれを読んでお勧めします.

    開発における弾丸の使用


    場合によっては、テストがカバレッジが低い場合など、以前に記載されている問題を検出できない場合がありますbullet 別のアプローチを使用して他の環境で.
    開発環境では、次の設定を有効にすることができます.
    Bullet.alert         = true
    
    次に、ブラウザでこのような警告を表示します.

    Bullet.add_footer    = true
    
    エラーのあるページにフッターを追加します.

    ブラウザのコンソールでエラーをログ出力することも可能です.
    Bullet.console    = true
    
    このようなエラーが追加されます.

    AppSignalでステージングで弾丸を使う


    ステージング環境では、これらのエラーメッセージをエンドユーザーに表示する必要はありませんが、アプリケーションが以前に述べた問題の1つを持っているかどうかを知るのは素晴らしいことです.
    同時にbullet アプリケーションのパフォーマンスを低下させ、メモリの消費量を増加させる可能性があります.
    ステージング環境が生産環境と同じ設定ファイルを使用していると仮定すると、それらの違いを減らすのに良い方法ですbullet 次のようになります.
    # config/environments/production.rb
    config.after_initialize do
      Bullet.enabled   = ENV.fetch('BULLET_ENABLED', false)
      Bullet.appsignal = true
    end
    
    あなたのステージング環境で見つかった問題に関する通知を受信するには、AppSignalを使用してエラーとして通知を報告できます.あなたが持っている必要がありますappsignal プロジェクトにインストールされ、設定されている宝石.あなたは、より多くの詳細を見ることができますRuby gem docs .
    その後、問題が検出された場合bullet , このようなエラーが発生します.

    このエラーはuniform_notifier gem から抽出したbullet .
    残念ながら、エラーメッセージには十分な情報が表示されませんがa Pull Request to improve this !

    結論


    The bullet GEMは、アプリケーションでパフォーマンスを低下させる問題を検出するのに役立つ素晴らしいツールです.
    前に述べたように良いテストカバレッジを維持してください.
    追加のヒントとして、データベースに関連するパフォーマンスの問題に対してさらに保護したい場合は、wt-activerecord-index-spy 適切なインデックスを使用していないクエリを検出するのに役立ちます.
    P . S .あなたが彼らがプレスから降りるとすぐに、ルビー魔法のポストを読みたいならば.subscribe to our Ruby Magic newsletter and never miss a single post !
    ファビオパーラーは、シニアソフトウェアエンジニアであり、企業は、15年以上の維持、スケーラブルで美しいソフトウェアを開発するために企業を支援しています.できます.