# ActiveRecordアンチパターン写経
参考記事
https://speakerdeck.com/toshimaru/active-record-anti-patterns
事例1 全Userの中から2017年以降の登録Userへ100ptを付与する。
アンチパターン① 全件取得&ループ
User.all.each do |user| if user.created_at >= Date.new(2017) user.point += 100 user.save end end
Model.allで全レコードを取得し、それにeachをかける。 →全件取得でメモリ逼迫 →ループ回数が増えCPUリソース消費
Better Way
User取得件数をフィルターして減らす all → where
少しずつUserを取得してメモリフレンドリーに each → find_each
User.where('created_at >= ?', Date.new(2017)).find_each do |user| user.point += 100 user.save end
アンチパターン② N+1 Update
# 1select User.where('created_at >= ?', Date.new(2017)).find_each do |user| user.point += 100 user.save # N update end
1回select + n回updateが走る Nの数が多くなればなるほどパフォーマンス悪化
Better Way
- 複数レコード一括更新
update -> update_all
User.where('created_at >= ?', Date.new(2017)).update_all('point = point + 100') end
実行速度爆速化!!!
※update と update_allは等価でない(callbackとか) ※テーブルロックに注意 →適切なトランザクションの設定・ロックの設定を!
事例2 ユーザー毎の記事のいいね数 合計が多い順にTOP100を出す!
アンチパターン③ Ruby Aggregation Pattern
user_like_counts = [] User.all.each do |user| user_like_counts << { name: user.name, total_like_count: user.posts.sum(&:like_count) } end user_like_counts .sort_by! { |u| u[:total_lik_count] } .reverse! .take(100).each do |u| puts "#{u[:name]} #{u[:total_like_count]}" end
Better Way
Post.group(:user_id) .select("user_id SUM(like_count) AS like_count") .order(like_count DESC) .limit(100).each do |post| puts "#{u[:name]} #{u[:total_like_count]}" end end
アンチパターン④ N+1あるでえええええ
Post.group(:user_id) .select("user_id SUM(like_count) AS like_count") .order(like_count DESC) .limit(100).each do |post| # ここやで!↓ puts "#{post.user.name} #{post.like_count]}" end end
Better Way
includesやで! joins, includes, preload, eager_loadで使い分けれるとよいね コードは省略