Type something to search...
如何使用 batch-loader gem 避免 N+1 Query

如何使用 batch-loader gem 避免 N+1 Query

  • Backend
  • 07 Jun, 2020

我一開始看 batch-loader 在 github 上的說明,一直看不懂到底該怎麼用,所以這邊紀錄一下它的用法。

這是一個基本的使用方式,可以從 accounts 從 DB 拿出對應的 users

def user_lazy(account)
  BatchLoader.for(account).batch do |accounts, loader|
    User.where(account: accounts).each do |user|
      loader.call(user.account, user)
    end
  end
end
users = accounts.map do |account|
  user_lazy(account)
end

首先,BatchLoader.for(account).batch{|..| ... } 會回傳一個 lazy object,也就是這個 code block 裡面的操作,並不會在呼叫 user_lazy method 時就立即被執行。

例如

users = accounts.map do |account|
  user_lazy(account)
end

不會真的執行 user_lazy 裡面的User.where(account: accounts)

而是等到真的需要使用到它的值,例如 puts users,才會真的對 DB query。

再來,

BatchLoader.for(account).batch do |accounts, loader|
  User.where(account: accounts).each do |user|
    loader.call(user.account, user)
  end
end

batch code block 的第一個參數 accounts,是透過從 BatchLoader.for(**account**) 蒐集到的account ,組合出 **accounts** 來讓 DB 可以一次 query(User.where(account: **accounts**))。

batch code block

User.where(account: accounts).each do |user|
  loader.call(user.account, user)
end

裡面的 loader.call(**user.account**, **user**) 做的事情。可以想像是把 User.where(account: accounts) 撈出來的 users ,拿來組出一個 hash,key 是 account ,value 是 user

而這個 hash 可以用來讓 BatchLoader.for(**account**) 利用 **account** 當作 key 從 hash 拿出所對應的 user

BatchLoader.for(account).batch do |accounts, loader|
  User.where(account: accounts).each do |user|
    loader.call(user.account, user)
  end
end

範例

假設你有個需求是根據一份名單,需要寫一段腳本讓使用者( user) 兌換兌換券(voucher)。

原始資料如下:

raw_data = [
  { account: 'shin', code: 'pmcHDn6aJAnTv7EU' },
  { account: 'en', code: 'RALyNNXKdx6WGkrs' },
]

兌換的功能由專屬的 service 來負責,執行的方式如下:

GiveVoucherService.new(user: user, voucher: voucher).execute

如果不考慮任何不良影響,可能會寫出這樣的程式碼:

data = raw_data.map do |d|
  {
    user: User.find_by(account: d[:account]),
    voucher: Voucher.find_by(code: d[:code]),
  }
end
data.each do |d|
  GiveVoucherService.new(user: d[:user], voucher: d[:voucher]).execute
end

但是觀察 log 後,應該可以明顯發現這樣會有 N+1 Query。

如果要避免 N+1 Query,概念上就是先把資料一次從資料庫撈出來,再來組織要給 service 的資料。

accounts = raw_data.map{ |d| d[:account] }
codes = raw_data.map{ |d| d[:code] }
hash_account_to_user = User.where(account: accounts).index_by(&:account)
hash_code_to_voucher = Voucher.where(code: codes).index_by(&:code)
data = raw_data.map do |d|
  {
    user: hash_account_to_user[d[:account]],
    voucher: hash_code_to_voucher[d[:code]],
  }
end

如果是用 batch-loader 的話,要怎麼做呢:

data = raw_data.map do |d|
  {
    user: user_lazy(d[:account]),
    voucher: voucher_lazy(d[:code]),
  }
end
def user_lazy(account)
  BatchLoader.for(account).batch do |accounts, loader|
    User.where(account: accounts).each do |user|
      loader.call(user.account, user)
    end
  end
end
def voucher_lazy(code)
  BatchLoader.for(code).batch do |codes, loader|
    Voucher.where(code: codes).each do |voucher|
      loader.call(voucher.code, voucher)
    end
  end
end

Related Posts

bullet gem 可能干擾對使用 preload 所造成效能影響的判定
bullet gem 可能干擾對使用 preload 所造成效能影響的判定

最近遇到了一個案例是,有個使用 includes 去 preload association 的地方,相對於直接 pluck 出來,花的時間慢的異常多。研究之後發現是 bullet 的 unused_eager_loading_enable 偵測打開後,會一定程度的影響效能。 如同這位開發者在 [issue](https://github.com/flyerhzm/bullet/issu

read more
CVE-2020–15169
CVE-2020–15169

來看看這個 CVE-2020–15169 漏洞是怎麼回事 看 https://github.com/advisories/GHSA-cfjv-5498-mph5 的說明可以知道,發生的時機在於,如果你使用了 [https://guides.rubyonrails.org/i18n.html#

read more
ActiveRecord 的 default_attributes
ActiveRecord 的 default_attributes

在 Rails server 沒有重開的情況下,更動資料庫表的欄位預設值,會有意想不到的事情發生 以下是用 Rails 6.0.3.2 + MySQL 5.6 實驗 事情是這樣的 有一張 tasks 表,有個欄位 completed,預設值為 false,且不能為 NULL create_table "tasks", options: "ENGI

read more
How to Update Your Pry Prompt Setting since v0.13.0
How to Update Your Pry Prompt Setting since v0.13.0

The pry gem changed itsPry::Prompt API since v0.13.0, and deprecated setting prompt through Pry.config.prompt = [] . If you have customized this way, you should use t

read more
ArgumentError: invalid byte sequence in UTF-8
ArgumentError: invalid byte sequence in UTF-8

有次遇到這個錯誤:ArgumentError: invalid byte sequence in UTF-8,一追之下發現是遇到類似這段程式碼, "english 中文\xED\xB6\xB0".gsub(/english/, '')會錯的原因是因為 \xED\xB6\xB0 不是合法的 UTF-8 編碼。 要檢查是不是合法的編碼可以用 `valid_encod

read more
method_missing 傳遞 splat argument 會噴 SystemStackError
method_missing 傳遞 splat argument 會噴 SystemStackError

最近遇到一個因為 splat argument 導致噴 SystemStackError 的案例,研究了半天發現是使用 method_missing 才會遇到 最近工作上遇到一個案例,是呼叫某個函式會噴 SystemStackError,google 發現了類似的事情 [https://github.com/redis/redis-rb/issues/264](https://github.co

read more
Rack 的 Middleware 的執行順序
Rack 的 Middleware 的執行順序

看〈為你自己學 Ruby on Rails〉介紹 Rack 的 Middleware 執行順序看不太懂,於是就研究了一下原始碼,看看是怎麼回事 因為常常聽到 Middleware Middleware 的,但是又不太懂,於是在網路上找到龍哥寫的文章,非常簡顯易懂,跟著動手做可以知道 Rack 要怎麼使用。 但是在「Middleware 的順序?」這的章節看不太懂盤子的比喻,於是決定直接在看原始

read more
[Rails][MySQL] 使用 update_all + JOIN + LIMIT 需注意效能問題
[Rails][MySQL] 使用 update_all + JOIN + LIMIT 需注意效能問題

MySQL 使用 JOIN + update_all 時,若遇到 LIMIT 則會轉為 subquery,需注意效能問題。 Rails 在使用 update_all 時,如果有用到 JOIN,會改用 subquery 的形式改寫,但若 Adapter 為 MySQL 時就會維持,因為 MySQL 語法本身支援 JOIN + UPDATE。但若是包含 LIMIT 時,由於 MySQL 也不支援,所

read more
Ruby Concurrency 筆記
Ruby Concurrency 筆記

Ruby 3.0.0 在去年聖誕節推出,release note 提到了一些 concurrency 相關的新功能,一邊研究一邊做個紀錄 在 Ruby 3.0.0 Released 提到了 Ruby 針對 Concurrency 加入了 [`Fiber#sch

read more
使用 Arel 寫出好看的 Left Join
使用 Arel 寫出好看的 Left Join

之前想要寫有條件的 left join 都會寫成 raw SQL query 的字串,但是其實也可以用 Arel 寫出比較好看的程式碼。 假設有個 DB 的 schema 長這樣: create_table "posts", force: :cascade do |t| t.string "title" t.boolean "active" t.integer "u

read more
在 Rails 5.1 之後,使用 pluck 不保證會 DB query
在 Rails 5.1 之後,使用 pluck 不保證會 DB query

在 Rails 5.1 之後,因為這個 commit,為 pluck 的行為帶來的一點變動。 因為習慣在 Rails 4.2 開發,養成了一個直覺是使用 pluck 就是會 DB query。但是就在換

read more
好像搞懂 searchkick 的 field 到底是怎麼用的了
好像搞懂 searchkick 的 field 到底是怎麼用的了

之前在看 searchkick 的 Github README 時,一直搞不懂 field 這個單位到底是什麼,於是花了一點時間來研究, gem 本人在此: searchkick 這個 gem 是封裝了幾個 Elasticsearch 相關的 gem,讓我們用 ActiveRecord 的方式,去操作 Elasticsearch 的 API。 <LinkCard url="https://

read more
如何 preload polymorphic associations
如何 preload polymorphic associations

因為想要用 includes preload polymorphic associations,但是不同 model 後面的 association 對象不一樣時不能直接用。後來在網路上找到可以用 ActiveRecord::Associations::Preloader preload 已經初始化後的 records 來達到目的。 **備註:Rails 6 已經支援 preload p

read more
如何在 debug test case 時印出 DB 的 log
如何在 debug test case 時印出 DB 的 log

有時候會想利用 test case 去追蹤一些操作 DB 的行為。 像是會放 binding.pry 在要觀察的程式附近,接著只要用 ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)就可以把操作 DB 的 log 印到 terminal 的畫面了。

read more
如何在 IRB/Pry/Rails Console 使用 Up/Down 鍵搜尋以前打過的命令
如何在 IRB/Pry/Rails Console 使用 Up/Down 鍵搜尋以前打過的命令

覺得 Ctrl + R 的 reverse-i-search 用的不是很習慣,google 了一下原來這樣設定就可以用 up/down 鍵搜尋了。 新增 ~/.inputrc 檔案,裡面設定: "\e[A": history-search-backward "\e[B": history-search-forward

read more
如何無痛跑 rake task
如何無痛跑 rake task

用 Rails console 開 sandbox 模式 $ rails c -s先載入環境就可以跑了 > Rails.application.load_tasks > Rake::Task['some_namespace:some_task_name'].invoke由於對 DB 的操作之後都會 rollback,所以可以拿來測試 rake

read more
如何讓 whenever 排程執行的程式,噴錯時寫到 Rollbar
如何讓 whenever 排程執行的程式,噴錯時寫到 Rollbar

串接 Rollbar 的好處是可以方便監控伺服器噴錯。但是在使用 whenever 排程時執行程式時,有辦法在噴錯時也寫到 Rollbar 嗎? 如果是使用 sidekiq,無論是使用 include Sidekiq::Worker 產生的 [worker](https://github.com/mperham/sidekiq/wiki/G

read more
從 Arel 看 Visitor Pattern
從 Arel 看 Visitor Pattern

偶然發現,Arel 好像是用 visitor pattern,好奇心驅使之下,就來研究看看。 翻翻歷史 翻了一下 Arel 的 repo,發現它其實一開始並不是用 visitor pattern 的,是在 2.0.0 之後才重寫,如這個 [History](https://github

read more
設定可以跑 rails core repository 測試的開發環境
設定可以跑 rails core repository 測試的開發環境

記錄一下照著官方教學設定開發 rails core repository 的環境 以下步驟取自 https://github.com/rails/rails-dev-box:安裝 VirtualBox。Mac 如果安裝 VirtualBox 遇到權限問題,[要到 Security & Privacy 允許

read more