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 (name, version) VALUES ('Widget', 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
Frequently Asked Questions
What happens when two processes update the same record at once?
The first UPDATE to commit changes the version value. The second UPDATE's WHERE clause still references the old version, so it matches zero rows and ObjectQuel throws an OrmException instead of silently overwriting the first process's change.
Which column types can back @Orm\Version?
Integer (starts at 1, increments by 1 per update), datetime/timestamp (set to the database's NOW() on each update), or UUID/GUID (a fresh identifier generated on each update).
Is the version column included in change detection?
No — version columns are excluded from change detection and managed automatically by ObjectQuel on every INSERT and UPDATE, so you never set the value yourself.
Does optimistic locking work with composite primary keys?
Yes, it's explicitly supported alongside single-column primary keys.