Ce qui se passe
Eloquent avait refresh() pour recharger un modèle depuis la base, et
lockForUpdate() pour poser un verrou de ligne dans une requête. Il manquait le
croisement des deux sur une instance déjà chargée. Laravel 13.27 ajoute
refreshForUpdate(), contribué par Steve Bauman dans la
PR #61247 :
public function refreshForUpdate()
{
if (! $this->exists) {
return $this;
}
return $this->refreshUsingQuery(
$this->newQueryWithoutScopes()->lockForUpdate()
);
}refreshUsingQuery() est l'auxiliaire déjà utilisé par refresh() : il cadre la
requête sur la clé du modèle, la fait passer par useWritePdo(), appelle
firstOrFail(), remplace les attributs bruts, recharge les relations déjà
chargées et resynchronise l'état d'origine. Le seul ajout de
refreshForUpdate(), c'est le verrou.
Ce que ça change
Avant, verrouiller un modèle déjà en main obligeait à le requêter de nouveau par sa clé :
DB::transaction(function () use ($product) {
$product = Product::query()
->lockForUpdate()
->findOrFail($product->getKey());
if ($product->stock === 0) {
throw new RuntimeException('The product is out of stock.');
}
$product->decrement('stock');
});Désormais :
DB::transaction(function () use ($product) {
$product->refreshForUpdate();
if ($product->stock === 0) {
throw new RuntimeException('The product is out of stock.');
}
$product->decrement('stock');
});Le détail qui compte est useWritePdo(). Sur une architecture avec réplicas de
lecture, un verrou pris sur un réplica ne protège rien : la valeur lue peut être
en retard, et l'écriture partira ailleurs. La méthode force la lecture sur la
connexion d'écriture.
Deux limites à garder en tête : la méthode renvoie l'instance telle quelle si le
modèle n'existe pas encore en base, et un verrou pessimiste n'a de sens que
dans une transaction — hors DB::transaction(), il est relâché aussitôt.
Source : Laravel News, Pessimistic Locking in Laravel Eloquent with refreshForUpdate()