veilletech.fr
29 août Feed du jour
#08 LARAVEL Article

Eloquent : refreshForUpdate pose enfin le verrou

Un verrou posé sur un réplica ne verrouille rien du tout.

Laravel 13.27 ajoute refreshForUpdate() à Eloquent : la méthode recharge un modèle par sa clé primaire avec un FOR UPDATE et met l'instance à jour sur place. Elle passe par le PDO d'écriture, ce qui évite qu'un réplica de lecture serve la valeur sur laquelle repose le verrou.

2 min de lecturevidéo 1:13
Partager
Sommaire3 sections
  1. Ce qui se passe
  2. Ce que ça change
  3. À retenir

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 :

PHP
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é :

PHP
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 :

PHP
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()