Skip to content

How-to : Contrôle de concurrence optimiste (champ version)

Référence FT : FT323 (NENE2-FT/optimisticlog) — API de document avec champ version dans le corps PUT, 409 sur version périmée, prévention des mises à jour perdues, 18 tests / 34 assertions PASS.

Ce guide montre comment implémenter le contrôle de concurrence optimiste en passant un champ version dans le corps de la requête, comme alternative aux en-têtes HTTP ETag/If-Match.

Schéma

sql
CREATE TABLE documents (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    body       TEXT    NOT NULL DEFAULT '',
    version    INTEGER NOT NULL DEFAULT 1,
    updated_at TEXT    NOT NULL
);

Endpoints

MéthodeCheminDescription
POST/documentsCréer (version commence à 1)
GET/documentsLister
GET/documents/{id}Obtenir avec version
PUT/documents/{id}Mettre à jour (version requise dans le corps)

Création

php
POST /documents  {"title": "Hello", "body": "World"}
 201  {"id": 1, "title": "Hello", "version": 1}

Mise à jour avec version

Le client lit la version courante et l'inclut dans le corps PUT :

php
// Lecture
GET /documents/1
 200  {"id": 1, "title": "Hello", "version": 1}

// Mise à jour avec version correcte
PUT /documents/1
{"title": "Updated", "body": "new body", "version": 1}
 200  {"id": 1, "title": "Updated", "version": 2}

La version s'incrémente à chaque mise à jour réussie.

Version périmée — 409 Conflict

php
// Alice et Bob lisent tous les deux la version 1
// Alice met à jour en premier → la version devient 2
// Bob essaie de mettre à jour avec la version 1 → rejeté
PUT /documents/1
{"title": "Bob's edit", "version": 1}
 409 Conflict  {"current_version": 2, "submitted_version": 1}

// Bob relit, obtient la version 2, réessaie
PUT /documents/1
{"title": "Bob's edit", "version": 2}
 200  {"version": 3}

Implémentation

php
private function update(ServerRequestInterface $request): ResponseInterface
{
    $body    = $this->parseBody($request);
    $version = $body['version'] ?? null;

    if (!is_int($version) || $version < 1) {
        return $this->json->create(['error' => 'version is required'], 422);
    }

    $doc = $this->repo->findById($id);
    if ($doc === null) {
        return $this->json->create(['error' => 'Not found'], 404);
    }

    if ($doc['version'] !== $version) {
        return $this->problems->create('conflict', 'Stale version', 409, [
            'current_version'   => $doc['version'],
            'submitted_version' => $version,
        ]);
    }

    $newVersion = $version + 1;
    // UPDATE documents SET ... WHERE id = ? AND version = ?
    $this->repo->update($id, $title, $newVersion, $now);

    return $this->json->create($updated);
}

La clause WHERE version = ? dans la requête UPDATE est le garde atomique contre les écritures concurrentes.

Version vs ETag

AspectChamp version (ce guide)ETag / If-Match (voir optimistic-locking-etag.md)
ProtocoleChamp de corpsEn-tête HTTP
UX client"version": N explicite en JSONEn-tête If-Match: "vN"
Payload 409Peut retourner current_version412 — pas de corps standard
Vérification manquante422 (version manquant)428 (If-Match manquant)

À ne pas faire

Anti-patternRisque
Accepter PUT sans champ versionMises à jour perdues : dernier qui écrit gagne silencieusement
Retourner 200 sur version périméeÉcrasement silencieux des changements concurrents
Vérifier la version uniquement dans le code applicatif (pas dans la clause WHERE)Condition de course entre lecture et écriture
Ne pas inclure current_version dans la réponse 409Le client doit re-GET pour récupérer ; l'inclure pour une réessai plus rapide

Publié sous licence MIT.