0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

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

最初はこのような実装になりました。
今回のテーマに関係の薄いところはコードを省略し、コメントのみ記載しています。

最初の実装
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の部分に注目してください。

    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()というメソッド一つでこんなに短くなるらしいです。

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

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

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

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

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

元のコードのログ
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回で済んでいます。

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によると

firstOrCreateメソッドは、指定したカラムと値のペアを使用してデータベースレコードを見つけようとします。モデルがデータベースで見つからない場合は、最初の配列引数をオプションの2番目の配列引数とマージした結果の属性を含むレコードが挿入されます。

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

亜種

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

感想

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

  1. ログを取った方法

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

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?