Rubyの隠れた宝石:弾丸
24230 ワード
データベースは多くのアプリケーションの中心です、そして、それに関する問題があることは重大なパフォーマンス問題を引き起こすかもしれません.
ActiveRecordとMongoidのようなOrmsは抽象的な実装を助け、コードをより速く提供しますが、時々、我々はどのようなクエリがフードの下で実行されているかをチェックすることを忘れてしまいます.
The bullet GEMは、いくつかのよく知られているデータベース関連の問題を識別できます. リストの各項目を読み込むクエリを実行するとき “未使用の熱心な負荷”:アプリケーションを読み込むときは通常、n + 1クエリを避けるために、それを使用していません 「欠落カウンタキャッシュ」:アプリケーションが関連項目の数を得るためにカウントクエリを実行する必要があるとき このポストでは、次のように表示します. 設定方法 前に述べた各問題の例. ハウ それぞれの問題を解決する方法 ハウツーとスタイル 私はいくつかの例を使用しますa project that I created for this post .
まず、宝石を追加する
私たちは与えられるすべての環境にそれを加えることができます、我々はそれを可能にするか、無効にすることができて、それぞれの上で異なるアプローチを使うことができます:
Railsプロジェクトにいる場合は、次のコマンドを実行して構成コードを自動的に生成できます.
以前に提案された設定で、Bulletはテストで実行される悪い質問を見つけて、それらのために例外を上げます.
さあ、例を見てみましょう.
与えられる
注意:controller tests are discouraged Rails 5以降
私は、それぞれのコントローラアクションの少なくとも1つの要求仕様を作成することを勧めます
以下を与えられます
問題を修正するには、上記のエラーの指示に従うだけで、クエリを削除することができます
このようなコントローラを指定します.
カウンタキャッシュは、ActiveRecordが関連するモデルを挿入して削除するときに自動的に更新されるテーブルに追加できる列です.詳細はthis post . 私はどのように作成し、カウンタキャッシュの同期を知るためにそれを読んでお勧めします.
場合によっては、テストがカバレッジが低い場合など、以前に記載されている問題を検出できない場合があります
開発環境では、次の設定を有効にすることができます.
ブラウザのコンソールでエラーをログ出力することも可能です.
ステージング環境では、これらのエラーメッセージをエンドユーザーに表示する必要はありませんが、アプリケーションが以前に述べた問題の1つを持っているかどうかを知るのは素晴らしいことです.
同時に
ステージング環境が生産環境と同じ設定ファイルを使用していると仮定すると、それらの違いを減らすのに良い方法です
その後、問題が検出された場合
このエラーはuniform_notifier gem から抽出した
残念ながら、エラーメッセージには十分な情報が表示されませんがa Pull Request to improve this !
The
前に述べたように良いテストカバレッジを維持してください.
追加のヒントとして、データベースに関連するパフォーマンスの問題に対してさらに保護したい場合は、wt-activerecord-index-spy 適切なインデックスを使用していないクエリを検出するのに役立ちます.
P . S .あなたが彼らがプレスから降りるとすぐに、ルビー魔法のポストを読みたいならば.subscribe to our Ruby Magic newsletter and never miss a single post !
ファビオパーラーは、シニアソフトウェアエンジニアであり、企業は、15年以上の維持、スケーラブルで美しいソフトウェアを開発するために企業を支援しています.できます.
ActiveRecordとMongoidのようなOrmsは抽象的な実装を助け、コードをより速く提供しますが、時々、我々はどのようなクエリがフードの下で実行されているかをチェックすることを忘れてしまいます.
The bullet GEMは、いくつかのよく知られているデータベース関連の問題を識別できます.
bullet
RubyプロジェクトのGEMbullet
検出します.bullet
を指定します.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年以上の維持、スケーラブルで美しいソフトウェアを開発するために企業を支援しています.できます.
Reference
この問題について(Rubyの隠れた宝石:弾丸), 我々は、より多くの情報をここで見つけました https://dev.to/appsignal/ruby-s-hidden-gems-bullet-22b7テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol