Transaction Model & Atomicity
In Enterprise applications, data integrity is non-negotiable. A classic example is saving an Order and its Line Items. You cannot have an Order created without its Items, nor can you deduct inventory if the Order fails to save.
ObjectQL provides a Declarative Transaction Protocol to handle these scenarios without writing complex imperative code.
1. The Atomic Mutation Protocol
Standard REST APIs often force you to make multiple requests (POST /orders, then loop POST /items). If the network fails halfway, you have "Zombie Data."
ObjectQL solves this with the Atomic Mutation Packet. You can bundle multiple operations into a single request. If any operation fails, the entire transaction rolls back.
Nested Mutations (Parent-Child)
The most common pattern is creating a Master record and its Details simultaneously.
Protocol Payload (JSON):
{
"operation": "create",
"object": "order",
"data": {
"customer": "cust_001",
"date": "2024-03-20",
"status": "draft",
// Nested writes are detected automatically
"lines": [
{
"product": "prod_A",
"quantity": 5,
"price": 100
},
{
"product": "prod_B",
"quantity": 2,
"price": 50
}
]
}
}
- Behavior: The ObjectQL Engine detects the
linesrelationship. It opens a Database Transaction, inserts the Order, gets the new_id, injects it into the Lines, inserts the Lines, and then Commits.
Batch Mutations (Mixed Objects)
Sometimes you need to update unrelated objects in one go (e.g., "Approve Invoice" AND "Deduct Budget").
Protocol Payload (JSON):
{
"operation": "transaction",
"steps": [
{
"operation": "update",
"object": "invoice",
"id": "inv_123",
"data": { "status": "approved" }
},
{
"operation": "update",
"object": "budget",
"id": "bdg_999",
"data": { "amount_remaining": { "$inc": -5000 } }
}
]
}
2. Optimistic Locking (Concurrency Control)
In a multi-user environment (e.g., two sales reps editing the same deal), "Last Write Wins" is dangerous. It leads to data loss.
ObjectQL enforces Optimistic Locking by default via the _version field.
The Mechanism
- Read: When you fetch a record, ObjectQL returns a
_versionnumber (e.g.,v1). - Write: When you update, you MUST send back the
_versionyou read. - Check: The database executes:
UPDATE ... WHERE id = '...' AND _version = 1. - Result:
- If rows affected = 1: Success.
_versionincrements to 2. - If rows affected = 0: Conflict Error. Someone else updated it to
v2already.
Protocol Example
Request:
{
"operation": "update",
"object": "deal",
"id": "deal_123",
"data": { "amount": 20000 },
"_version": 5 // I believe I am editing version 5
}
Response (Error):
{
"error": {
"code": "VERSION_CONFLICT",
"message": "Record has been modified by another user.",
"current_version": 6
}
}
:::tip UI Handling
ObjectUI handles this automatically. If it receives a VERSION_CONFLICT, it prompts the user to "Refresh and Re-apply".
:::
3. Server-Side Transaction Hooks
Sometimes, the logic is too complex for the client. You need to define transaction boundaries on the server.
You can define Transactional Hooks in your *.trigger.ts files. These hooks run inside the database transaction scope.
// src/triggers/order.trigger.ts
import { Trigger } from '@objectql/types';
export const afterOrderApprove: Trigger = {
on: 'update',
object: 'order',
when: (newDoc, oldDoc) => newDoc.status === 'approved' && oldDoc.status !== 'approved',
// This function receives a 'session' which is bound to the DB transaction
handler: async ({ id, session, broker }) => {
// 1. Create Shipment (Must succeed)
await broker.call('data.create', {
object: 'shipment',
data: { order_id: id }
}, { session });
// 2. Notify Warehouse (Side effect, does not block transaction)
await broker.emit('warehouse.notify', { order_id: id });
// If Step 1 throws an error, the Order update is ALSO rolled back.
}
}
4. Isolation Levels
ObjectQL generally defaults to Read Committed isolation level for relational databases.
- Dirty Reads: Prevented. You never see uncommitted data from other transactions.
- Non-Repeatable Reads: Allowed.
- Phantom Reads: Allowed.
For scenarios requiring strictly serialized access (e.g., generating sequential Invoice Numbers), use the Atomic Increment operator or a dedicated Sequence Object, rather than locking the whole table.
// Atomic Increment Protocol
{
"operation": "update",
"object": "counter",
"id": "invoice_seq",
"data": { "seq": { "$inc": 1 } }
}
Summary
| Feature | Description | Benefit |
|---|---|---|
| Nested Mutation | Save Parent + Children in one JSON. | Prevents orphan records. |
| Transaction Packet | Bundle mixed operations. | Ensures cross-module consistency. |
| Optimistic Locking | _version checking. | Prevents lost updates in multi-user apps. |
| Hooks | Server-side logic in transaction. | Guarantees business rule integrity. |