Ruby on Railsで開発をしていると、ActiveRecordのfind
メソッドとfind_by
メソッドの使い分けに迷うことがありませんか?
この2つのメソッドは一見似ていますが、適切に使い分けないと以下のような問題が発生しかねません。
- 不適切なエラー処理によるアプリのクラッシュ
- 非効率なデータベースクエリの発行によるパフォーマンス低下
- コードの可読性、保守性の低下
この記事を最後まで読めば、find
メソッドとfind_by
メソッドの明確な違いを理解し、適切に使い分けられるようになります。
この記事を書いているぼくは実務経験1年。独学で未経験から従業員300名以上の自社開発企業へ転職しました。実務ではVue.jsとRailsを毎日書いています。
【Rails】find と find_by の3つの違い
find
メソッドとfind_by
メソッドの違いは以下の3点です。
- 指定できるカラムが異なる
- レコードが存在しなかった場合の挙動が異なる
- 取得するレコードの件数が異なる
順番に解説します。
1. 指定できるカラムが異なる
find メソッドの場合
find
メソッドはUser.find(1)
のように主キー(id)を指定してレコードを検索します。
# id を指定して検索する
irb(main):001> User.find(1)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=>
#<User:0x0000000100e10658
id: 1,
name: "Suzuki Taro",
email: "taro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00>
find_by メソッドの場合
find_by
メソッドはUser.find_by(email: "test@example.com")
のように、主キー以外でも任意のカラムを指定して検索可能です(もちろんid
も指定できる)
# id 以外のカラムを指定して検索できる
irb(main):001> User.find_by(email: "taro@example.com")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "taro@example.com"], ["LIMIT", 1]]
=>
#<User:0x0000000100e10658
id: 1,
name: "Suzuki Taro",
email: "taro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00>
# もちろん id も指定できる
irb(main):002> User.find_by(id: 1)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=>
#<User:0x0000000100e10658
id: 1,
name: "Suzuki Taro",
email: "taro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00>
2. レコードが存在しなかった場合の挙動が異なる
find メソッドの場合
find
メソッドはレコードが見つからなかった場合、ActiveRecord::RecordNotFound
例外を発生させます。
# id: 999 に紐づくレコードが存在しない場合、例外発生
irb(main):001> User.find(999)
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 999], ["LIMIT", 1]]
(irb):1:in `<main>': Couldn't find User with 'id'=999 (ActiveRecord::RecordNotFound)
find_by メソッドの場合
find_by
メソッドはレコードが見つからなかった場合、例外は発生させずnil
を返します。
# email: "hogehoge@example.com" に紐づくレコードが存在しない場合、nil を返す
irb(main):001> User.find_by(email: "hogehoge@example.com")
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["email","hogehoge@example.com"], ["LIMIT", 1]]
=> nil
3. 取得するレコードの件数が異なる
find メソッドの場合
find
メソッドの引数で複数のIDを指定した場合、オブジェクトの配列を返します。
その際、指定したIDに紐づくレコードが1件でも存在しなければ、ActiveRecord::RecordNotFound
例外を発生させます。
# 複数のIDを指定した場合、User オブジェクトの配列を返す
irb(main):001> User.find([1, 2])
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?) [["id", 1], ["id", 2]]
=>
[#<User:0x0000000107fd8fd8
id: 1,
name: "Suzuki Taro",
email: "taro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00>
#<User:0x0000000107fd8e98
id: 2,
name: "Sasaki Jiro",
email: "jiro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:20:17.842224000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:20:17.842224000 UTC +00:00>]
# 指定したIDに紐づくレコードが1つでも存在しなければ例外発生(id = 3 のレコードが存在しない)
irb(main):002> User.find([1, 3])
Author Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?) [["id", 1], ["id", 3]]
(irb):7:in `<main>': Couldn't find all Users with 'id': (1, 3) (found 1 results, but was looking for 2). (ActiveRecord::RecordNotFound)
find_by メソッドの場合
find_by
メソッドの引数で指定した条件に一致するレコードが複数ある場合、最初に見つかったレコードのみを返します。
# verified = true のレコードは2件あるが最初の1件だけを取得する
irb(main):001> User.find_by(verified: true)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."verified" = ? LIMIT ? [["verified", true], ["LIMIT", 1]]
=>
#<User:0x0000000100e10658
id: 1,
name: "Suzuki Taro",
email: "taro@example.com",
verified: true,
created_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00,
updated_at: Sun, 23 Jun 2024 11:19:36.535648000 UTC +00:00>
find メソッドと find_by メソッドの使い分け
ここまででfind
メソッドとfind_by
メソッドの違いを理解できたかと思います。
しかし、「結局のところ両者をどのように使い分けたらいいのか?」と感じている人も多いはずです。
そこでここからはfind
メソッドとfind_by
メソッドの使い分けについて、実務経験をもとに話していきます。
find メソッドを使うべき場面
find
メソッドが適切な場面として以下の4つが挙げられます。
- レコードが必ず存在することが期待される場合
- 主キーによる検索の場合
- レコードが見つからない場合にエラー処理を行いたい場合
- 複数のレコードを一度に取得したい場合
順番に説明を加えます。
レコードが必ず存在することが期待される場合
たとえば、ユーザー認証後のプロフィール表示時などがこれに該当します。
ログインしているユーザーのプロフィールを表示する際、そのユーザーのレコードは必ず存在するはずです。
def show
@user = User.find(current_user.id)
# プロフィール表示のロジック
end
このように使用することで、万が一レコードが見つからない場合には例外が発生し、想定外の状況を早期に検出できます。
主キーによる検索の場合
URLパラメータなどから得た主キー(id)でレコードを検索する場合はfind
メソッドが最適です。
def show
@article = Article.find(params[:id])
# 記事表示のロジック
end
前述したようにfind
メソッドが主キーでの検索に特化しているためです。
レコードが見つからない場合にエラー処理を行いたい場合
たとえば、存在しない記事のIDがURLで指定された場合に404エラーページへリダイレクトさせたい場合などです。
find
メソッドはレコードが見つからない場合にActiveRecord::RecordNotFound
例外を発生させるため、この例外をキャッチしてエラーページへリダイレクトさせる処理を簡単に実装できます。
def show
@article = Article.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to error_path, status: :not_found
end
複数のレコードを一度に取得したい場合
複数のIDを配列で指定することで、それらのIDに対応するレコードをまとめて取得できます。
def bulk_update
@users = User.find([1, 2, 3])
# 複数ユーザーの一括更新ロジック
end
これは、複数のユーザー情報を一度に表示したり更新したりする必要がある場合などに便利です。
ただし、指定したIDのうち1つでも存在しないレコードがある場合は例外が発生するので注意が必要です。
find_by メソッドを使うべき場面
find_by
メソッドが適切な場面として以下の4つが挙げられます。
- 主キー以外のカラムで検索する場合
- レコードが存在しない可能性があり、その場合も正常系として扱いたい場合
- 複数の条件で検索したい場合
- 最初に見つかったレコードのみが必要な場合
主キー以外のカラムで検索する場合
ユーザーのメールアドレスや、商品コードなど、主キー以外の一意な識別子で検索する場合にfind_by
メソッドは有効です。
def login
@user = User.find_by(email: params[:email])
# ログイン処理のロジック
end
レコードが存在しない可能性があり、その場合も正常系として扱いたい場合
例えば、ユーザー登録時の重複チェックなどがこれに該当します。
def create
if User.find_by(email: params[:email])
flash[:alert] = "このメールアドレスは既に登録されています"
render :new
else
# ユーザー登録処理
end
end
この場合、ユーザーが存在しなくても例外は発生せず、単にnil
が返されるため、スムーズに処理を続行できます。
複数の条件で検索したい場合
find_by
メソッドは複数の条件を指定して検索することができます。
def search
@user = User.find_by(email: params[:email], active: true)
# 検索結果の処理
end
これにより、アクティブなユーザーの中から特定のメールアドレスを持つユーザーを検索するといった複雑な条件での検索が可能になります。
最初に見つかったレコードのみが必要な場合
find_by
メソッドは条件に合致する最初のレコードのみを返します。
これは、ユニークな属性による検索や、複数のレコードが存在する可能性があるが1つだけ取得すれば十分な場合に適しています。
def find_by_username
@user = User.find_by(username: params[:username])
# ユーザー情報の表示ロジック
end
パフォーマンス面の注意
最後にパフォーマンス面についても触れておきます。
find
メソッドとfind_by
メソッドのパフォーマンスは使用状況によって異なります。
find メソッド
find
メソッドは常に主キー(id)で検索するため、インデックスが効きます。
そのため検索パフォーマンスはとても高速です。
find_by メソッド
find_by
メソッドの場合、指定したカラムにインデックスが貼られているかどうかで大きくパフォーマンスが変わります。
たとえばUser.find_by(email: "taro@example.com")
の場合、内部的には次のSQLを生成するからです。
SELECT * FROM users WHERE email = 'taro@example.com' LIMIT 1
email
カラムにインデックスが貼られておらず、users
テーブルに大量のレコードが格納されているケースでは検索パフォーマンスが悪化してしまいます。
条件指定の仕方を変えるか、事前にインデックスを追加しておくなどの対応をしてください。
おわりに
今回はActiveRecordのfind
メソッドとfind_by
メソッドの違いと使い分けについて解説しました。
両者の違いをもう一度おさらいします。
- 指定できるカラムが異なる
- レコードが存在しなかった場合の挙動が異なる
- 取得するレコードの件数が異なる
どちらもRuby on Railsの開発では100%使いますのでしっかりマスターしておきましょう。
また、以下の記事ではワンランク上のRailsエンジニアになりたいと考えている方向けにおすすめの技術書を紹介しています。
こちらの記事もぜひ読んでみてください。
コメント