ActiveRecord クエリ
Ruby on Rails
where・join・scope・N+1対策
基本クエリ
where・order・limit・find・pluck
# 検索
User.all # 全レコード(遅延評価)
User.find(1) # 主キーで検索(なければ例外)
User.find_by(email: 'a@b.com') # 最初の1件(なければ nil)
User.find_by!(email: 'a@b.com') # なければ例外
User.first; User.last
User.first(3) # 最初の3件
# where
User.where(active: true)
User.where(role: ['admin', 'moderator']) # IN
User.where(age: 18..65) # BETWEEN
User.where('age >= ?', 18) # プレースホルダー(推奨)
User.where('created_at > ?', 1.week.ago)
User.where('name LIKE ?', "%#{query}%")
User.where.not(role: 'guest')
User.where(active: true).or(User.where(role: 'admin'))
# 並べ替え・制限
User.order(:name) # 昇順
User.order(name: :desc) # 降順
User.order(role: :asc, name: :desc) # 複数
User.limit(10)
User.limit(10).offset(20) # ページネーション
User.limit(10).offset(page * 10)
# 集計
User.count
User.count(:email) # NULLを除いたカウント
User.where(active: true).count
User.average(:age)
User.sum(:balance)
User.minimum(:age); User.maximum(:age)
# select・pluck
User.select(:id, :name, :email) # 指定カラムのみ取得
User.pluck(:email) # 値の配列 ['a@b.com', ...]
User.pluck(:id, :name) # [[1,'Alice'],[2,'Bob'],...]
User.ids # [1, 2, 3, ...]
# 存在確認
User.exists?(1)
User.exists?(email: 'a@b.com')
User.where(active: true).exists?JOIN・includes・N+1対策
eager loading・joins・left_joins
# N+1 問題
# ❌ N+1: posts を取得後、各 post で user を個別にクエリ
posts = Post.all
posts.each { |p| puts p.user.name } # N+1!
# ✅ includes: 関連を事前ロード
posts = Post.includes(:user)
posts.each { |p| puts p.user.name } # クエリ2回で済む
# 複数の関連を同時にロード
Posts.includes(:user, :tags, comments: :user)
# joins: INNER JOIN(関連レコードが存在するもののみ)
Post.joins(:user).where(users: { active: true })
Post.joins(:comments).distinct # コメントがある投稿
Post.joins(comments: :likes) # ネストした joins
# left_joins: LEFT OUTER JOIN(関連がなくても取得)
Post.left_joins(:comments)
.select('posts.*, COUNT(comments.id) AS comment_count')
.group('posts.id')
# eager_load: includes + LEFT OUTER JOIN(WHERE に関連を使う場合)
Post.eager_load(:user).where(users: { role: 'admin' })
# preload: includes の内部実装(常に別クエリ)
Post.preload(:user)
# ベンチマーク用クエリ確認
Post.includes(:user).to_sql # 発行されるSQLを確認
# Bullet gem: N+1 を自動検出(開発環境)
# gem 'bullet', group: :developmentスコープ
scope・default_scope・クエリオブジェクト
class Post < ApplicationRecord
# スコープ(再利用可能なクエリ)
scope :published, -> { where(status: 'published') }
scope :draft, -> { where(status: 'draft') }
scope :recent, -> { order(created_at: :desc) }
scope :popular, -> { order(views_count: :desc) }
scope :featured, -> { where(featured: true) }
# 引数付きスコープ
scope :by_author, ->(user) { where(user: user) }
scope :since, ->(date) { where('created_at >= ?', date) }
scope :limit_to, ->(n) { limit(n) }
# スコープのチェーン
# Post.published.recent.limit(10)
# Post.by_author(user).since(1.month.ago).popular
# default_scope(注意して使う)
default_scope { where(deleted_at: nil) } # ソフトデリート
# default_scope は unscoped で解除できる
# Post.unscoped.where(...)
# クラスメソッドはスコープと同様にチェーン可能
def self.top(n = 10)
popular.limit(n)
end
end
# クエリオブジェクト(複雑なクエリを分離)
class PostSearchQuery
attr_reader :relation
def initialize(relation = Post.all)
@relation = relation
end
def call(params)
result = relation
result = result.where('title ILIKE ?', "%#{params[:q]}%") if params[:q].present?
result = result.where(status: params[:status]) if params[:status].present?
result = result.by_author(params[:author]) if params[:author].present?
result = result.since(params[:from].to_date) if params[:from].present?
result.recent
end
end
# 使用
posts = PostSearchQuery.new.call(q: 'Rails', status: 'published')トランザクションと高度なクエリ
transaction・upsert・bulk insert・生SQL
# トランザクション
ActiveRecord::Base.transaction do
from_account.update!(balance: from_account.balance - amount)
to_account.update!(balance: to_account.balance + amount)
end
# いずれかで例外が発生するとロールバック
# ネストしたトランザクション(savepoint)
User.transaction do
user.save!
User.transaction(requires_new: true) do
profile.save!
rescue ActiveRecord::RecordInvalid
# 内側のみロールバック
end
end
# bulk insert(Rails 6+)
Posts.insert_all([
{ title: 'Post 1', user_id: 1, created_at: Time.current, updated_at: Time.current },
{ title: 'Post 2', user_id: 2, created_at: Time.current, updated_at: Time.current },
])
# upsert(Rails 6+)
User.upsert_all(
[{ email: 'a@b.com', name: 'Alice' }],
unique_by: :email
)
# update_all / delete_all(コールバックを呼ばない)
Post.where(status: 'draft').update_all(status: 'archived')
Post.where('created_at < ?', 1.year.ago).delete_all
User.where(active: false).destroy_all # コールバックあり(遅い)
# 生SQL
ActiveRecord::Base.connection.execute('SELECT 1')
User.find_by_sql('SELECT * FROM users WHERE age > 18')
User.where('age > ?', 18).to_sql # SQLを確認
# explain
User.where(active: true).explain # クエリ実行計画