Optimistic Locking
Two requests often load the same row, change it, and save it. Without a check, the second save silently overwrites the first one. Optimistic locking prevents this with a version column. Each update checks that the row still has the version that was loaded, and it writes the next version. When another request has already changed the row, the update fails with an OptimisticLockException instead of overwriting the data.
No lock is held in the database while the user is editing, so the approach suits web forms, where the time between the read and the write can be long.
Minimal Setup
Add a property with #[OptimisticLock]. The integer version is the recommended type.
use Windwalker\ORM\Attributes\Column;
use Windwalker\ORM\Attributes\OptimisticLock;
#[Column('version')]
#[OptimisticLock]
public int $version = 0;Showcase
Two editors open the same article. The first save succeeds. The second save fails, because its version is out of date:
use Windwalker\ORM\Exception\OptimisticLockException;
$first = $orm->mustFindOne(Article::class, 1);
$second = $orm->mustFindOne(Article::class, 1);
$first->title = 'Changed by the first editor';
$orm->updateOne($first);
$second->title = 'Changed by the second editor';
try {
$orm->updateOne($second);
} catch (OptimisticLockException $e) {
// Reload the row, show the conflict, and let the user retry.
}The first update writes version 1. The second update finds version 1 in the row, but it still holds version 0, so it changes nothing and throws the exception.
Version Types
The next value depends on the type of the property:
intadds1to the previous value.floatadds1.0to the previous value.stringgenerates a new value withuid().DateTimeInterfaceis set to the current time.
Other types throw a RuntimeException, unless you provide a nextHandler. Use the int type in most cases.
#[Column('version')]
#[OptimisticLock]
public int $version = 0;INFO
An integer is not affected by the precision of a column, and two updates in the same second still get different versions. A time-based version works only with a column that stores microseconds, such as DATETIME(6) in MySQL or timestamp(6) in PostgreSQL. A column with seconds precision truncates the value, so the next update fails against the value that was stored.
Custom Next Value
Pass a callable as nextHandler to generate the next value yourself. It receives the current value and returns the next one.
PHP attribute arguments must be constant expressions, so a closure or an arrow function cannot be used here. Refer to a public static method with a callable array, using self::class for the class that declares the property:
use Windwalker\ORM\Attributes\Column;
use Windwalker\ORM\Attributes\OptimisticLock;
class Article
{
#[Column('hash')]
#[OptimisticLock(nextHandler: [self::class, 'nextHash'])]
public string $hash = '';
public static function nextHash(mixed $value): string
{
return hash('sha256', uniqid());
}
}WARNING
The method must be public static. It receives the current value and must return the next value. A closure or an arrow function in the attribute is a fatal error when the class is loaded.
How the Check Works
On update, the ORM adds a condition on the version column to the WHERE clause, and it writes the next version. If no row matches, the exception is thrown. A version of 0 is a valid value, and it is included in the condition. Only null skips the check.
WARNING
The check applies only when the version is part of the data being written. An array update without the version column is not checked, and it overwrites the row. Load the entity, or include the version in the array, so the lock is applied.
Inserts are not checked. When an entity is new, the version is written as it is.
Disabling the Check
Pass optimisticLock: false in ORMOptions for one write when the check must be skipped, for example in a background job that owns the row.
use Windwalker\ORM\ORMOptions;
$orm->updateOne(
$article,
options: new ORMOptions(optimisticLock: false),
);The write then overwrites any change made by another request, so use this option only when no other writer can change the row.