あれば取得し、なければ作るという処理を書きたい
私は、記事の投稿時にタグをつけられる機能を実装していました。
ユーザには、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回で済んでいます。
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を見て引き出しを増やしておきたいと思いました。
-
ログを取った方法
↩DB::enableQueryLog(); // ここからクエリログの記録を開始 // ここにログを見たい処理 $queries = DB::getQueryLog(); // 記録されたクエリを取得 DB::disableQueryLog(); // ログ記録を停止 (任意) dd($queries); // ここでクエリログを出力して確認