Skip to content

Como Fazer: Controle de Concorrência Otimista (Campo de Versão)

Referência FT: FT323 (NENE2-FT/optimisticlog) — API de documentos com campo de versão no corpo do PUT, 409 em versão obsoleta, prevenção de perda de atualização, 18 testes / 34 asserções PASS.

Este guia mostra como implementar controle de concorrência otimista passando um campo version no corpo da requisição, como alternativa aos headers HTTP ETag/If-Match.

Schema

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étodoCaminhoDescrição
POST/documentsCriar (versão começa em 1)
GET/documentsListar
GET/documents/{id}Obter com versão
PUT/documents/{id}Atualizar (versão obrigatória no corpo)

Criação

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

Atualização com Versão

O cliente lê a version atual e a inclui no corpo do PUT:

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

// Atualizar com versão correta
PUT /documents/1
{"title": "Updated", "body": "new body", "version": 1}
 200  {"id": 1, "title": "Updated", "version": 2}

A versão incrementa em cada atualização bem-sucedida.

Versão Obsoleta — 409 Conflict

php
// Alice e Bob leem a versão 1
// Alice atualiza primeiro → versão torna-se 2
// Bob tenta atualizar com versão 1 → rejeitado
PUT /documents/1
{"title": "Bob's edit", "version": 1}
 409 Conflict  {"current_version": 2, "submitted_version": 1}

// Bob lê novamente, obtém versão 2, tenta novamente
PUT /documents/1
{"title": "Bob's edit", "version": 2}
 200  {"version": 3}

Implementação

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);
}

A cláusula WHERE version = ? na query UPDATE é a guarda atômica contra escritas concorrentes.

Versão vs ETag

AspectoCampo de Versão (este guia)ETag / If-Match (veja optimistic-locking-etag.md)
ProtocoloCampo do corpoHeader HTTP
UX do cliente"version": N explícito em JSONHeader If-Match: "vN"
Payload do 409Pode retornar current_version412 — sem padrão de corpo
Verificação ausente422 (faltando version)428 (faltando If-Match)

O Que NÃO Fazer

Anti-padrãoRisco
Aceitar PUT sem campo versionPerda de atualização: última escrita vence silenciosamente
Retornar 200 em versão obsoletaSobrescrita silenciosa de mudanças concorrentes
Verificar versão apenas no código da aplicação (não na cláusula WHERE)Condição de corrida entre leitura e escrita
Não incluir current_version na resposta 409Cliente deve fazer GET novamente para recuperar; incluir para retry mais rápido

Publicado sob a licença MIT.