Last active
September 12, 2023 13:51
-
-
Save troatie/def0fba42fcfb70f873b7f033fbe255f to your computer and use it in GitHub Desktop.
Guard against race conditions in Laravel's firstOrCreate and updateOrCreate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
trait CreatesWithLock | |
{ | |
public static function updateOrCreate(array $attributes, array $values = []) | |
{ | |
return static::advisoryLock(function () use ($attributes, $values) { | |
// emulate the code found in Illuminate\Database\Eloquent\Builder | |
return (new static)->newQuery()->updateOrCreate($attributes, $values); | |
}); | |
} | |
public static function firstOrCreate(array $attributes, array $values = []) | |
{ | |
return static::advisoryLock(function () use ($attributes, $values) { | |
return (new static)->newQuery()->firstOrCreate($attributes, $values); | |
}); | |
} | |
/** | |
* In my project, this advisoryLock method actually lives as a function on the global namespace (similar to Laravel Helpers). | |
* In that case the $lockName, and default lock duration are pased in as arguments. | |
*/ | |
private static function advisoryLock(callable $callback) | |
{ | |
// Lock name based on Model. | |
$lockName = substr(static::class . ' *OrCreate lock', -64); | |
// Lock for at most 10 seconds. This is the MySQL >5.7.5 implementation. | |
// Older MySQL versions have some weird behavior with GET_LOCK(). | |
// Other databases have a different implementation. | |
\DB::statement("SELECT GET_LOCK('" . $lockName . "', 10)"); | |
$output = $callback(); | |
\DB::statement("SELECT RELEASE_LOCK('" . $lockName . "')"); | |
return $output; | |
} | |
} |
This has been completely resolved since Laravel 10.22.0, Please read it through translation: [Laravel] createOrFirst の登場から激変した firstOrCreate, updateOrCreate に迫る!
If retry processing under unique key constraints is not sufficient: mpyw/laravel-database-advisory-lock: Advisory Locking Features for Postgres/MySQL/MariaDB on Laravel
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice solution but I'd use a cache lock instead of a db lock https://laravel.com/docs/10.x/cache#atomic-locks