firstOrCreate()でif文やクエリ発行回数を減らそう！

# あれば取得し、なければ作るという処理を書きたい

私は、記事の投稿時にタグをつけられる機能を実装していました。

ユーザには、`input`タグに、スペース区切りでタグ名を入れてもらいます。
それがすでにテーブルに存在するタグだったら、その`id`を記事に紐づける。
まだないタグだったら、新しくタグを作成の上、その`id`を記事に紐づける。
という処理をやりたいです。

最初はこのような実装になりました。
今回のテーマに関係の薄いところはコードを省略し、コメントのみ記載しています。
```php:最初の実装
public function store(ArticleStoreRequest $request) {

    // バリデーション済みデータから記事を作成

    // inputタグに入力された文字列から、余分な空白や重複などを取り除く
    
    // その文字列から、配列$tag_namesを作成

    // 記事に紐づけたいタグのidを入れるための配列を用意
    $tag_ids = [];

    foreach ($tag_names as $tag_name) {
        // タグが存在すればそのidを配列$tag_ids[]に入れる。
        if (Tag::where('name', $tag_name)->exists()) {
            $tag = Tag::where('name', $tag_name)->first();
            $tag_ids[] = $tag->id;
        // タグがなければ、作成してから、そのidを配列$tag_ids[]に入れる。
        } else {
            $new_tag = Tag::create([
                'name' => $tag_name,
            ]);
            $tag_ids[] = $new_tag->id;
        }
    }

    //　配列に入れたidをまとめて中間テーブルに登録する
    $article->tags()->attach($tag_ids);

    return to_route('articles.index');
}
```
この`foreach`の部分に注目してください。
```php
    foreach ($tag_names as $tag_name) {
        if (Tag::where('name', $tag_name)->exists()) {
            $tag = Tag::where('name', $tag_name)->first();
            $tag_ids[] = $tag->id;
        } else {
            $new_tag = Tag::create([
                'name' => $tag_name,
            ]);
            $tag_ids[] = $new_tag->id;
        }
    }
```
みなさん、私みたいにこんな`if`文書いていませんか？
AIに指摘されて知ったのですが、これ、`firstOrCreate()`というメソッド一つでこんなに短くなるらしいです。

```php:改善後のコード   
    foreach ($tag_names as $tag_name) {
        $tag = Tag::firstOrCreate([
            'name' => $tag_name,
        ]);
        $tag_ids[] = $tag->id;
    }
```

`if`文がなくなり、ずいぶんコードがすっきりしました。


また、タグが存在する場合のクエリ発行回数も減っています。

「芸術」という元からあるタグと、「政治・経済」という今はないタグの2つをつけた場合のログを見てみましょう。[^1]

元のコードでは、存在確認の`exists()`で1回、データ取得の`first()`で1回の、計2回、クエリが発行されています。

```shell:元のコードのログ
array:4 [▼ // app\Http\Controllers\ArticleController.php:110

# タグが存在する場合

  # 存在確認で1回
  0 => array:3 [▼
    "query" => "select exists(select * from `tags` where `name` = ?) as `exists`"
    "bindings" => array:1 [▼
      0 => "芸術"
    ]
    "time" => 0.34
  ]
  
  # 取得で1回
  1 => array:3 [▼
    "query" => "select * from `tags` where `name` = ? limit 1"
    "bindings" => array:1 [▼
      0 => "芸術"
    ]
    "time" => 0.28
  ]


# タグがない場合

  # 存在確認で1回
  2 => array:3 [▼
    "query" => "select exists(select * from `tags` where `name` = ?) as `exists`"
    "bindings" => array:1 [▼
      0 => "政治・経済"
    ]
    "time" => 0.23
  ]
  
  # タグ作成で1回
  3 => array:3 [▼
    "query" => "insert into `tags` (`name`, `updated_at`, `created_at`) values (?, ?, ?)"
    "bindings" => array:3 [▼
      0 => "政治・経済"
      1 => "2025-06-07 09:37:45"
      2 => "2025-06-07 09:37:45"
    ]
    "time" => 2.08
  ]
]
```

一方で、`firstOrCreate()`を使えば、これが1回で済んでいます。
```shell:firstOrCreteを使った場合のログ
array:3 [▼ // app\Http\Controllers\ArticleController.php:110

# タグが存在する場合
  # 取得で1回
  0 => array:3 [▼
    "query" => "select * from `tags` where (`name` = ?) limit 1"
    "bindings" => array:1 [▼
      0 => "芸術"
    ]
    "time" => 0.42
  ]


# タグがない場合

  # 取得で1回(何も取得されない)
  1 => array:3 [▼
    "query" => "select * from `tags` where (`name` = ?) limit 1"
    "bindings" => array:1 [▼
      0 => "政治・経済"
    ]
    "time" => 0.33
  ]

  # タグ作成で1回
  2 => array:3 [▼
    "query" => "insert into `tags` (`name`, `updated_at`, `created_at`) values (?, ?, ?)"
    "bindings" => array:3 [▼
      0 => "政治・経済"
      1 => "2025-06-07 09:33:14"
      2 => "2025-06-07 09:33:14"
    ]
    "time" => 2.57
  ]
]
```

タグが存在しない場合は、どちらも2回クエリが発行されますので違いはありません。

`firstOrCreate()`は、[Readouble](https://readouble.com/laravel/12.x/ja/eloquent.html#:~:text=%E3%81%AE%E5%8F%96%E5%BE%97%EF%BC%8F%E7%94%9F%E6%88%90-,firstOrCreate,-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AF%E3%80%81%E6%8C%87%E5%AE%9A)によると
>firstOrCreateメソッドは、指定したカラムと値のペアを使用してデータベースレコードを見つけようとします。モデルがデータベースで見つからない場合は、最初の配列引数をオプションの２番目の配列引数とマージした結果の属性を含むレコードが挿入されます。

とのこと。
私が求めていたとおり、あればそれを取得し、なければ新しく挿入してくれます。
名前のとおり、`first()`と`create()`を合体させたメソッドのようですね。
第二引数については、取得時には使わず、挿入時には使いたい要素がある場合に便利なようです。

# 亜種
- `firstOrNew()`: インスタンスを生成するだけで、後で`save`が必要
- `updateOrCreate()`: 更新するか、なければ新たに生成し保存


# 感想
どれもメソッド名でなんとなく挙動わかるのがよいですね。
今回紹介したものは、自分では存在に気づいていなかったメソッドなので、たまにReadoubleを見て引き出しを増やしておきたいと思いました。


[^1]:ログを取った方法
    ```php
    DB::enableQueryLog();  // ここからクエリログの記録を開始
    
    // ここにログを見たい処理
    
    $queries = DB::getQueryLog(); // 記録されたクエリを取得
    DB::disableQueryLog(); // ログ記録を停止 (任意)
    dd($queries);  // ここでクエリログを出力して確認
    ```