7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MetapsAdvent Calendar 2024

Day 21

ActiveRecordの便利なクエリメソッド集

Last updated at Posted at 2024-12-20

こんにちは。アドベントカレンダー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

実際に

スクリーンショット 2024-12-20 16.15.04.png

# 内部結合
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メソッドと組み合わせて使用する方法などありますので気になった方は!

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?