ぎんさんマインド

いちエンジニアの思考とか趣味についてつらつらと書いてみるかもしれない。

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