-
-
Save troatie/def0fba42fcfb70f873b7f033fbe255f to your computer and use it in GitHub Desktop.
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; | |
} | |
} |
Great approach to the problem, thank you.
Is that a solution for laravel/framework#19372 ? Maybe make a pull request? @troatie
@amenk problem is this is MySQL-specific. For Laravel to accept the PR, it would need to be DB agnostic.
This is great, a big help.
Nice solution, It however only solves it when on the model itself. In our use case we also call updateOrCreate
on relationships.
What kind of problems could arise from using a lock on the DB? Wouldn't this be a problem for high traffic applications?
Nice solution but I'd use a cache lock instead of a db lock https://laravel.com/docs/10.x/cache#atomic-locks
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
Big fan of this approach - glad to see you mention it over at https://murze.be/breaking-laravels-firstorcreate-using-race-conditions
Just a quick clarification for any passer-bys: The second parameter is not a timeout on the obtained lock, but a timeout on trying to obtain the lock. Once you hold the lock it is valid until you release it or connection closes. -1 is used to wait indefinitely, 0 just executes a check and moves on, much like a Redis lock would.