【Rails】findとfind_byの違いと最適な使い分け

Ruby on Railsで開発をしていると、ActiveRecordのfindメソッドとfind_byメソッドの使い分けに迷うことがありませんか?

この2つのメソッドは一見似ていますが、適切に使い分けないと以下のような問題が発生しかねません。

  • 不適切なエラー処理によるアプリのクラッシュ
  • 非効率なデータベースクエリの発行によるパフォーマンス低下
  • コードの可読性、保守性の低下

この記事を最後まで読めば、findメソッドとfind_byメソッドの明確な違いを理解し、適切に使い分けられるようになります。

この記事を書いているぼくは実務経験1年。独学で未経験から従業員300名以上の自社開発企業へ転職しました。実務ではVue.jsとRailsを毎日書いています。

この記事で解説するRailsのバージョンは7.1.3.4です!

目次

【Rails】find と find_by の3つの違い

findメソッドとfind_byメソッドの違いは以下の3点です。

  1. 指定できるカラムが異なる
  2. レコードが存在しなかった場合の挙動が異なる
  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メソッドの違いと使い分けについて解説しました。

両者の違いをもう一度おさらいします。

  1. 指定できるカラムが異なる
  2. レコードが存在しなかった場合の挙動が異なる
  3. 取得するレコードの件数が異なる

どちらもRuby on Railsの開発では100%使いますのでしっかりマスターしておきましょう。

また、以下の記事ではワンランク上のRailsエンジニアになりたいと考えている方向けにおすすめの技術書を紹介しています。

こちらの記事もぜひ読んでみてください。

あわせて読みたい
【実務経験1年以上向け】Rubyで設計を学べる技術書3選 Rubyで設計を学べる技術書を知りたくないですか?この記事ではRubyエンジニア向けにサンプルコードがRubyで書かれていて設計を学べる技術書を3つ紹介しています。自分に必要なのはどの1冊なのか、ぜひ考えてみてください。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

未経験でSESから従業員300名以上の自社開発企業に転職しました。業務や個人開発で直面した問題や、転職・学習の経験を発信していきます。

コメント

コメントする

目次