rubyのBatchLoaderを使ってみた
私はrubyでGraphQLを何度か触れていたけれども、以前からN+1問題に何度か手を焼いていて、ちょうど仕事でそれの対応がてらBatchLoaderを使うことがあった。
ただ調べた感じ日本語の記事はほとんどなくて使い方とかもそんななかったのでここに残しておく
BatchLoaderとは
GitHub - exAspArk/batch-loader: Powerful tool for avoiding N+1 DB or HTTP queries
簡単に言えばN+1問題を対策するためのgem
N+1問題の対策としてよく使われるのはbulletで検知してpreloadするというものだと思うが、こちらはメソッドを指定して明示的に対策できる。
インストール
いつものようにgemfileに
gem 'batch-loader'
もしくは
gem install batch-loader
rails appで使う場合は
config/application.rb に
class Application < Rails::Application config.middleware.use BatchLoader::Middleware end
graphqlの場合は
class GraphQL::Schema use BatchLoader::GraphQL end
簡単な使い方
コードを出すとこんな感じ
class User has_many :posts end class Post belongs_to :user end
みたいな時に
BatchLoader.for(post.user_id).batch do |user_ids, loader| User.where(id: user_ids).each { |user| loader.call(user.id, user) } end
で
SELECT * FROM posts WHERE id IN (1, 2, 3) SELECT * FROM users WHERE id = 1 SELECT * FROM users WHERE id = 2 SELECT * FROM users WHERE id = 3
が対策できる。
また、batchの引数でちょっとした設定を追加できる。
例として
batch(default_value: [])
なら初期値を[]に設定できる。
さらにN+1が起こりうる処理が複数ある場合は
user = User.where(id: [1,2,3]) key ={ hoge: user.hoge, # N+1が起こる処理1 fuga: user.fuga, # N+1が起こる処理2 hide: user.hide # N+1が起こる処理2 } BatchLoader.for(key).batch do |hoges, fugas, hides, loader| loader.call(user.hoges, user) loader.call(user.fugas, user) loader.call(user.hides, user) end
一応このような使い方ができる。
ただ、起こりうるメソッドの内部に埋め込む方がいい感じの形のように思う。
つまり
class User def hoge(post) BatchLoader.for(post.user_id).batch do |user_ids, loader| where(id: user_ids).each { |user| loader.call(user.id, user) } end end end User.hoge(post)
の方がおそらく開発者の意図に沿っていると思う。
オプション
引数* | デフォルト値* | 説明* |
---|---|---|
item | - | バッチ処理に使用されるアイテム。 |
default_value | nil | デフォルト値 |
cache | true | 同実行間のキャッシュを無効にするなら false |
replace_methods | true | バッチング後にメソッドを置き換えるのではなく、#method_missingを使用する場合はfalseを設定します。 |
key | nil | バッチブロックを一意に識別するためのカスタムキーを渡します。 |
items | - | バッチ処理のために収集されたアイテムのリスト |
loader | - | バッチで読み込まれた値をロードするために呼び出されるべきラムダ |
args | {default_value: nil, cache: true, replace_methods: true, key: nil} | バッチメソッドに渡される引数 |
(readmeからdeelp翻訳)
どういう時に使うべきなのか
おそらく基本的にはpreloadしていく方が楽だと思う。例なんてまさにそうで、一文加えてばいいだけの話だろってなるだろう。
ただそれでも複雑に絡み合ったテーブル設計になってしまった場合はpreloadしていくのも辛くなってくる。
その中で、要件上速度を落とせない部分、N+1が起こった場合バグだったりユーザーから大きく不評をえてしまう様な処理を書く際に、
preloadして対策したけど思わぬquery発行があって遅くなってしまったなんてことが起こらないよう
BatchLoaderでこの処理だけは絶対にN+1が起こらない様にすることで安全に作ることが出来る。
参考資料
みんな大好き週間Rails 日本語だとここくらいしか見つからなかった。
techracho.bpsinc.jp
上リンクで書かれていた有名そうな記事(英語)
engineering.universe.com
サポートしてる GitLabで書かれている記事 英文でGraphQL向けだけれども読みやすい
docs.gitlab.com
最後に公式
github.com