Model::updateOrCreate()
は楽観ロック的な実装なので,ユニークキー制約は必須です。愚直に
- 「SELECT してきて存在しなかったら INSERT」
- 「SELECT してきて存在したら UPDATE」
というのをやっているだけで, SELECT FOR UPDATE 的な悲観ロック処理は実装されていません。
おすすめの対処法
1. ユニークキー制約を張る
重複インサートを防ぐために必須で,これは次の 2 番のための準備でもあります。
2. ユニークキー制約エラーを検出して自動的に1回までリトライする
楽観ロックでの実行で悲観ロックでの実行と同等の信頼性を得るためにはこうします。
<?php
namespace App\Database;
use Illuminate\Database\MySqlConnection as BaseMySqlConnection;
use Illuminate\Support\Str;
use Throwable;
class MySqlConnection extends BaseMySqlConnection
{
/**
* INSERTで重複が発生したとき,自動的に再試行します。
* 以下のメソッドと併せて使用することを想定しています。
*
* - Model::firstOrNew()
* - Model::firstOrCreate()
* - Model::updateOrCreate()
* - Model::sync()
* - Model::syncWithoutDetaching()
*
* @param \Closure $closure
* @param int $attempts
* @return mixed
*/
public function retryWhenDuplicateEntry(\Closure $closure, int $attempts = 2)
{
$e = null;
for ($i = max(1, $attempts); $i > 0; --$i) {
try {
return $closure();
} catch (QueryException $e) {
if (!Str::contains($e->getMessage(), 'Duplicate entry')) {
break;
}
// リトライ時には必ずマスタを参照する
$this->recordsModified = true;
}
}
throw $e;
}
}
<?php
namespace App\Providers;
use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
Connection::resolverFor('mysql', function (...$parameters) {
return new MySqlConnection(...$parameters);
});
}
}
これで
DB::retryWhenDuplicateEntry(function () {
Model::updateOrCreate(...);
});
と書くだけです。
(IDE で補完を出すためには barryvdh/laravel-ide-helper のファサード補完を併用してください)
このコード何回も書いてるのでそろそろ OSS 化しようかな…