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

## はじめに
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]). ‘ID` refers 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3301763/60c5c421-47c1-e11f-2341-b17a72d3ac9b.png)

### findを使用した場合
```Ruby
  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
```shell
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を使用した場合
```Ruby
  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
```shell
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回のクエリで取得できるようになりました
```sql
WHERE `comments`.`id` = 11 AND `comments`.`memo_id` = 2 LIMIT 1
```

### 発行されるクエリの回数が減ることで何が良いか？
**データベースへのアクセス回数が減ることでパフォーマンスが向上する**DBに対してクエリを送信した際には以下のようなことが行われます
1. APサーバ(Rails)のModelがSQLを発行
1. DBサーバがSQLを受け取るとパーサー、オプティマイザが解析を行い実行計画を作る
1. 作成された実行計画をもとに処理を実行
1. 処理結果を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` である可能性を考慮し、適切にハンドリングする必要があります。例えば、以下のようにします

```ruby
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.

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

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

## 参考資料

https://api.rubyonrails.org/v7.1.3.4/classes/ActiveRecord/FinderMethods.html#method-i-find

https://www.amazon.co.jp/SQL%E3%81%AE%E8%8B%A6%E6%89%8B%E3%82%92%E5%85%8B%E6%9C%8D%E3%81%99%E3%82%8B%E6%9C%AC-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%93%8D%E4%BD%9C%E3%81%8C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%81%A7%E3%81%8D%E3%82%8C%E3%81%B0%E8%AA%B0%E3%81%A7%E3%82%82%E3%81%A7%E3%81%8D%E3%82%8B-Software-Design-plus%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/4297107171