Optimistic locking¶
Optimistic locking prevents lost updates in concurrent systems without holding a database lock for the duration of a read-modify-write cycle. Each row carries a version counter; when you write back, alchemiq checks that the counter still matches what you read. If another process already incremented it, you receive a conflict exception instead of silently overwriting their change.
Enabling versioning¶
Set versioned = True in the model’s inner Meta class:
from alchemiq import Model
from alchemiq.types import PK
class Order(Model):
id: PK[int]
status: str
total: int
class Meta:
versioned = True
Alchemiq injects a _version column (BIGINT, non-nullable,
server_default 1 so the counter starts at 1) automatically, backed by
SQLAlchemy’s native version_id_col mechanism.
The single-underscore name avoids collisions with a business-level version
field you might declare yourself.
Reading the current version¶
Use version_of() to read the _version value from a fetched
instance:
from alchemiq import Repository, version_of
from myapp.models import Order
orders = Repository(Order)
order = await orders.get(id=1)
ver = version_of(order) # e.g. 2
version_of() raises ConfigError if the model was not
declared with Meta.versioned = True.
Conditional update and delete¶
Pass the version you observed as expected_version (keyword-only) to
update() or delete(). Alchemiq checks it before the flush:
# Update - fails if the row was modified since order was fetched
await orders.update(1, expected_version=ver, status="paid")
# Delete - same guard applies
await orders.delete(1, expected_version=ver)
If the check fails, ConcurrentModificationError is raised
and nothing is written. Map it to an HTTP 409 response:
from alchemiq import ConcurrentModificationError
from fastapi import HTTPException
try:
await orders.update(1, expected_version=ver, status="paid")
except ConcurrentModificationError:
raise HTTPException(status_code=409, detail="Order was modified by another request.")
Even without an explicit expected_version, SQLAlchemy’s native
version_id_col increments _version on every flush and raises
StaleDataError if the counter was already bumped by a concurrent
transaction. Alchemiq translates that into ConcurrentModificationError
as well.
Bulk operations bypass the version check¶
bulk_create(), filter().update(), and filter().delete() operate at
the SQL level and do not enforce the version counter. Use single-row
update() / delete() whenever optimistic concurrency matters.