4
3

【Rails】findとfind_byの違いを理解する

Last updated at Posted at 2024-08-24

はじめに

Railsでデータベース操作を効率的に行うためfindとfind_byの違いを調べたのでメモとして残します!
それぞれのメソッドの動作や使いどころを簡単な例を通じてお伝えできればと思います。

両者の違い

findメソッド

Rialsの公式より

find(*args)
Find by id - This can either be a specific id (ID), a list of ids (ID, ID, ID), or an array of ids ([ID, ID, ID]). ‘IDrefers to an “identifier”. For models with a single-column primary key,ID` will be a single value, and for models with a composite primary key, it will be an array of values. If one or more records cannot be found for the requested ids, then ActiveRecord::RecordNotFound will be raised. If the primary key is an integer, find by id coerces its arguments by using to_i.

  • ID(主キー)で検索を行う(他の属性は指定できない)
  • 該当する結果がない場合「ActiveRecord::RecordNotFound (例外)」発生させる
  • 単一のレコードを返す

find_byメソッド

find_by(arg, *args)
Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
If no record is found, returns nil.

  • 指定された条件で検索を行う(ID以外も指定可能、複数条件も指定可能)
  • 条件に合致する最初の1件を返す(順序は保証されない、順序を決めたい場合自身で指定する必要がある)
  • 該当する結果がない場合Nilを返す ⇦結果が無の場合の挙動がfindと違うので注意

では具体的にどう使い分けを行うのか?

find_byのが良いケース

例:メモテーブルのレコードに紐づくコメントテーブルのレコードを削除する場合

ER図

image.png

findを使用した場合

  def destroy
    memo = Memo.find(params[:memo_id])
    comment = memo.comments.find(id: params[:id])

    if comment.destroy
      head :no_content
    else
      render json: { errors: comment.errors.full_messages }, status: :unprocessable_entity
    end
  end

以下のような順番でクエリが発行される
①メモテーブルから引数のidに合致するレコードを取得
②コメントテーブルからメモオブジェクトのidと引数のidに合致するレコードを取得
③取得したコメントのレコードを削除する

クエリ(SQL)が3回も発行されることになります

実際に発行されたSQL

Started DELETE "/memos/2/comments/10" 
Processing by CommentsController#destroy as */*
Parameters: {"memo_id"=>"2", "id"=>"10"}
#メモの取得
Memo Load (1.5ms)  SELECT `memos`.* FROM `memos` WHERE `memos`.`id` = 2 LIMIT 1

#コメントの取得
Comment Load (1.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`memo_id` = 2 AND `comments`.`id` = 10 LIMIT 1

TRANSACTION (0.4ms)  BEGIN

#コメントの削除
Comment Destroy (2.5ms)  DELETE FROM `comments` WHERE `comments`.`id` = 10
 TRANSACTION (4.3ms)  COMMIT
Completed 204 No Content in 17ms (ActiveRecord: 9.8ms | Allocations: 2864)

find_byを使用した場合

  def destroy
    comment = Comment.find_by!(id: params[:id], memo_id: params[:memo_id])

    if comment.destroy
      head :no_content
    else
      render json: { errors: comment.errors.full_messages }, status: :unprocessable_entity
    end
  end

以下のような順番でクエリが発行される
①コメントテーブルから引数のmemo_id、idに合致するレコードを取得
②取得したコメントのレコードを削除する
今回で言うとパラメータとしてcommentテーブルのidとmemo_idが渡ってきています。
この2の値があればcommentテーブルから一意にレコードを特定できるので2回クエリを発行する必要はありません。

実際に発行されたSQL

Started DELETE "/memos/2/comments/11" 
Processing by CommentsController#destroy as */*
  Parameters: {"memo_id"=>"2", "id"=>"11"}
  # コメントの取得
  Comment Load (2.2ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`id` = 11 AND `comments`.`memo_id` = 2 LIMIT 1
  TRANSACTION (0.4ms)  BEGIN

  # コメントの削除
  Comment Destroy (2.9ms)  DELETE FROM `comments` WHERE `comments`.`id` = 11
  TRANSACTION (2.9ms)  COMMIT
Completed 204 No Content in 15ms (ActiveRecord: 8.4ms | Allocations: 2468)

WHERE句で複数条件を指定することで1回のクエリで取得できるようになりました

WHERE `comments`.`id` = 11 AND `comments`.`memo_id` = 2 LIMIT 1

発行されるクエリの回数が減ることで何が良いか?

データベースへのアクセス回数が減ることでパフォーマンスが向上するDBに対してクエリを送信した際には以下のようなことが行われます

  1. APサーバ(Rails)のModelがSQLを発行
  2. DBサーバがSQLを受け取るとパーサー、オプティマイザが解析を行い実行計画を作る
  3. 作成された実行計画をもとに処理を実行
  4. 処理結果をAPサーバに返却する

この2のSQLの解析、実行計画の作成は重い処理になっています。
そしてクエリ発行の度にDBサーバ、APサーバ間は通信を行います。

たかが1回の差と思うかも知れませんが、ユーザ数が増え、アクセス数が多くなったり、データ量の多いレコードだった場合など影響は大きくなります。

find_byを使用する時の注意点

レコードを取得し、結果がなかった場合の挙動がfindと少し異なるため注意が必要です
findの場合は、結果が0の場合、レコードが存在しないことを表すエラー「ActiveRecord::RecordNotFound (例外)」発生させます

find_byの場合はエラーではなくNilを返します。
よってその違いを理解してエラーハンドリングをしないと「NoMethodError」が起きてしまいます。

     NoMethodError:
       undefined method `destroy' for nil:NilClass

結果が存在しなかった場合、Nilクラスに対してdestroyメソッドを呼び出すことになるためです。

find_by 使用時のエラーハンドリング

結果が nil である可能性を考慮し、適切にハンドリングする必要があります。例えば、以下のようにします

comment = Comment.find_by(id: params[:id], memo_id: params[:memo_id])
if comment
  # レコードが見つかった場合の処理
else
  # レコードが見つからなかった場合の処理
  render json: { error: 'Comment not found' }, status: :not_found
end

またはメソッドの末尾に「!」をつけることでfindと同様に「ActiveRecord::RecordNotFound (例外)」発生させるようにできます

find_by!(arg, *args)Link
Like find_by, except that if no record is found, raises an ActiveRecord::RecordNotFound error.

    comment = Comment.find_by!(id: params[:id], memo_id: params[:memo_id])

まとめ

以上です!
クエリの発行回数を減らしてパフォーマンスを向上させたいときや、条件に応じたレコードの存在を確認したいときには find_by が便利だと思いました。今回紹介した2つのメソッド以外にも、いろいろなメソッドや処理の違いを理解することで、状況に応じた適切な使い分けができるように引き続き学んでいきたいです!

参考資料

4
3
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
4
3