Optimistic Locking

The @Orm\Version annotation provides optimistic locking for ObjectQuel entities, preventing lost updates when multiple processes modify the same record concurrently.

What is Optimistic Locking?

Optimistic locking detects conflicts at commit time rather than locking records upfront. When multiple processes attempt to modify the same entity, only the first succeeds—subsequent attempts fail with a clear version mismatch error.

/**
 * @Orm\Table(name="products")
 */
class ProductEntity {
    /**
     * @Orm\Column(name="id", type="integer", unsigned=true, primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    protected ?int $id = null;

    /**
     * @Orm\Column(name="version", type="integer", unsigned=true)
     * @Orm\Version
     */
    protected ?int $version = null;
}

The @Orm\Version annotation marks a column for automatic version tracking. ObjectQuel handles all version updates automatically.

Why Use Optimistic Locking?

Optimistic locking prevents data loss in concurrent scenarios:

  • Detect conflicts - Know immediately when another process modified your data
  • No blocking - Unlike pessimistic locks, reads and writes don't block each other
  • Automatic handling - ObjectQuel manages version increments transparently
  • Clear errors - Explicit exceptions when conflicts occur, allowing retry logic
  • Safe concurrency - Multiple users can work simultaneously without corrupting data

Supported Version Types

ObjectQuel supports three column types for version tracking:

// Integer version - increments by 1
/**
 * @Orm\Column(name="version", type="integer", unsigned=true)
 * @Orm\Version
 */
protected ?int $version = null;

// Datetime version - updates to current timestamp
/**
 * @Orm\Column(name="version", type="datetime")
 * @Orm\Version
 */
protected ?\DateTime $version = null;

// UUID version - generates new GUID
/**
 * @Orm\Column(name="version", type="uuid")
 * @Orm\Version
 */
protected ?string $version = null;
  • Integer - Starts at 1, increments by 1 on each update
  • Datetime/Timestamp - Uses database NOW() function
  • UUID/GUID - Generates a new globally unique identifier

How It Works

ObjectQuel automatically manages version columns during persistence:

On INSERT

-- ObjectQuel generates
INSERT INTO products SET name='Widget', version=1

On UPDATE

-- ObjectQuel generates
UPDATE products
SET name='Updated Widget', version=version + 1
WHERE id=123 AND version=5

-- If version changed, zero rows affected → OrmException thrown

The WHERE clause includes the current version value. If another process already updated the record, the version won't match and the update affects zero rows, triggering an exception.

Technical Details

  • Version columns are excluded from change detection
  • Updates use SQL expressions (no application round-trip required)
  • Post-operation SELECT synchronizes in-memory entity with database state
  • Version values are normalized according to property type annotations
  • Works with single and composite primary keys