こんにちは。アドベントカレンダー21日目です。
今回、RailsでDB操作を行う際に欠かせないActiveRecord。クエリメソッドを活用することで、コードを簡潔かつ効率的に記述することができます。特によく使う便利なクエリメソッドを具体例とともに紹介します。
where
特定の条件に一致するレコードを取得するための基本メソッド
# ユーザー名が "hoge" のレコードを取得
User.where(name: "hoge")
# 年齢が20以上のユーザーを取得
User.where("age >= ?", 20)
# 複数条件
User.where(name: "hoge", status: "active")
find_by
最初に一致する1件のレコードを取得します。該当しない場合はnilを返す
# ユーザー名が "hoge" の最初のレコードを取得
User.find_by(name: "hoge")
where.firstとの違いは、find_byが内部でLIMIT 1を自動的に追加する点
pluck
特定のカラムだけを配列として取得
# ユーザーの全名前を配列で取得
User.pluck(:name)
# 複数カラムを取得
User.pluck(:name, :email)
必要なデータだけを取得するので、メモリ使用量が減少。
オブジェクト化されないため高速。
order
取得したデータをソート
# 年齢の昇順でソート
User.order(:age)
# 年齢の降順でソート
User.order(age: :desc)
# 複数カラムでソート
User.order(:status, age: :desc)
limit と offset
取得するレコード数や開始位置を指定
# 最初の5件を取得
User.limit(5)
# 6件目以降のレコードを取得
User.offset(5)
# ページネーションの実装例
User.order(:created_at).limit(10).offset(20)
exists?
条件に一致するレコードが存在するかを確認
# ユーザー名が "hoge" のレコードが存在するか
User.exists?(name: "hoge") # true または false
find_each
大量のデータを扱う際、バッチ処理で効率的にレコードを処理
# 全ユーザーを100件ずつ処理
User.find_each(batch_size: 100) do |user|
puts user.name
end
メモリ消費を抑えながら大量データを処理可能
group と count
# ステータスごとのユーザー数をカウント
User.group(:status).count
# 結果: { "active" => 10, "inactive" => 5 }
select
必要なカラムだけを取得し、オブジェクト化します。
# id と name だけを取得
User.select(:id, :name)
ActiveRecordオブジェクトになるため、pluckの方が速い
update_all
複数レコードを一括更新
# ステータスを全て "inactive" に更新
User.where(status: "active").update_all(status: "inactive")
コールバックは実行されない
joins
関連テーブルを結合してクエリを実行
# books テーブルと users テーブルを結合して投稿タイトルとユーザー名を取得
Post.joins(:user).select('books.title, users.name')
joinsとleft_outer_joinsの使い分け
内部結合(joins)
joinsは関連付けられたテーブルのデータが存在する場合にのみレコードを取得
# 投稿に関連するユーザー情報を取得(関連のない投稿は除外)
Post.joins(:user).select("posts.title, users.name")
生成されるSQL
SELECT posts.title, users.name
FROM posts
INNER JOIN users ON users.id = posts.user_id
ユーザー情報が必須で結果に「関連がないデータ」を含めたくない場合に使用する
外部結合 left_outer_joins
left_outer_joinsは、関連が存在しない場合でも親テーブルのデータをすべて取得
# 全ての投稿を取得し、関連するユーザー名も取得(関連がない場合はNULL)
Post.left_outer_joins(:user).select("posts.title, users.name")
生成されるSQL
SELECT posts.title, users.name
FROM posts
LEFT OUTER JOIN users ON users.id = posts.user_id
実際に
# 内部結合
Post.joins(:user).select("posts.title, users.name")
# => [["Hoge", "Hideki"], ["Piyo", "yoshio"]]
# 外部結合
Post.left_outer_joins(:user).select("posts.title, users.name")
# => [["Hoge", "Hideki"], ["Fuga", nil], ["Piyo", "yoshio"]]
includes
N+1問題を防ぐために、関連テーブルを事前に読み込むメソッド
# N+1問題を回避してユーザーの投稿を取得
User.includes(:posts).each do |user|
puts user.posts.map(&:title)
end
joinsとの違い
joins: データベース結合
joinsはデータベースレベルでINNER JOINやLEFT OUTER JOINを使い、関連テーブルを結合します。SQLクエリ内で結合が行われるため、取得されるデータはすべてデータベースで計算・フィルタリングされる
includes: メモリ内結合
includesは、関連データを別々のクエリで事前に取得し、Rubyのメモリ内で関連付けを行います。これは主にN+1問題を防ぐために使用
includesはN+1問題を防ぐために、関連データを別々のクエリで事前取得します。その後、Rubyのメモリ内で関連付けを行う
N1が発生する場合
User.all.each do |user|
puts user.posts.map(&:title)
end
includesを使用すると
User.includes(:posts).each do |user|
puts user.posts.map(&:title)
end
この場合生成されるsql
SELECT * FROM users;
SELECT * FROM posts WHERE posts.user_id IN (1, 2, 3, ...);
必要なデータを最初に全て取得するため、クエリ数が減少するが、必要以上のデータを取得する可能性があり、メモリ消費が増加する可能性
そのほかにもincludesメソッドと組み合わせて使用する方法などありますので気になった方は!