Minstrel

Ruby, JavaScript, Haskell, Math, Music, Design

# 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

  1. User取得件数をフィルターして減らす all → where

  2. 少しずつ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

  1. 複数レコード一括更新

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で使い分けれるとよいね コードは省略