Skip to content

Commit 421fddd

Browse files
authored
docs: Added documentation on DB row locking functionality (#419)
1 parent 3f7cf43 commit 421fddd

File tree

1 file changed

+115
-0
lines changed

1 file changed

+115
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Row locking
2+
3+
Row-level locking allows you to lock specific rows in the database to prevent other transactions from modifying them while you work. This is essential for safely handling concurrent updates, such as processing payments, managing inventory, or any scenario where two transactions might conflict.
4+
5+
:::warning
6+
All row locking operations require a [transaction](transactions). An exception will be thrown if you attempt to acquire a lock without one.
7+
:::
8+
9+
For the following examples we will use this model:
10+
11+
```yaml
12+
class: Company
13+
table: company
14+
fields:
15+
name: String
16+
```
17+
18+
## Locking rows with a read
19+
20+
You can lock rows as part of a read operation by passing the `lockMode` parameter to `find`, `findFirstRow`, or `findById`. The locked rows are returned and held until the transaction completes.
21+
22+
```dart
23+
await session.db.transaction((transaction) async {
24+
var companies = await Company.db.find(
25+
session,
26+
where: (t) => t.name.equals('Serverpod'),
27+
lockMode: LockMode.forUpdate,
28+
transaction: transaction,
29+
);
30+
31+
// Rows are locked — safe to update without conflicts.
32+
for (var company in companies) {
33+
company.name = 'Updated name';
34+
await Company.db.updateRow(session, company, transaction: transaction);
35+
}
36+
});
37+
```
38+
39+
When a row is locked, other transactions that attempt to acquire a conflicting lock on the same rows will wait until the lock is released. Regular reads without a `lockMode` are not affected and can still read the rows freely. If waiting is not desired, you can configure the [lock behavior](#lock-behavior) to either throw an exception immediately or skip locked rows.
40+
41+
The `findFirstRow` and `findById` methods also support locking. Here's an example using `findById`:
42+
43+
```dart
44+
await session.db.transaction((transaction) async {
45+
var company = await Company.db.findById(
46+
session,
47+
companyId,
48+
lockMode: LockMode.forUpdate,
49+
transaction: transaction,
50+
);
51+
52+
if (company != null) {
53+
company.name = 'Updated name';
54+
await Company.db.updateRow(session, company, transaction: transaction);
55+
}
56+
});
57+
```
58+
59+
## Locking rows without fetching data
60+
61+
If you only need to lock rows without reading their data, use the `lockRows` method. This acquires locks with less overhead since no row data is transferred.
62+
63+
```dart
64+
await session.db.transaction((transaction) async {
65+
await Company.db.lockRows(
66+
session,
67+
where: (t) => t.name.equals('Serverpod'),
68+
lockMode: LockMode.forUpdate,
69+
transaction: transaction,
70+
);
71+
72+
// Rows are locked — perform updates using other methods.
73+
});
74+
```
75+
76+
## Lock modes
77+
78+
The `lockMode` parameter determines the type of lock acquired. Different lock modes allow varying levels of concurrent access.
79+
80+
| Lock Mode | Constant | Description |
81+
|---|---|---|
82+
| For update | `LockMode.forUpdate` | Exclusive lock that blocks all other locks. Use when you intend to update or delete the locked rows. |
83+
| For no key update | `LockMode.forNoKeyUpdate` | Exclusive lock that allows `forKeyShare` locks. Use when updating non-key columns only. |
84+
| For share | `LockMode.forShare` | Shared lock that blocks exclusive locks but allows other shared locks. Use when you need to ensure rows don't change while reading. |
85+
| For key share | `LockMode.forKeyShare` | Weakest lock that only blocks changes to key columns. |
86+
87+
For a detailed explanation of how lock modes interact, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS).
88+
89+
## Lock behavior
90+
91+
The `lockBehavior` parameter controls what happens when a requested row is already locked by another transaction. If not specified, the default behavior is to wait.
92+
93+
| Behavior | Constant | Description |
94+
|---|---|---|
95+
| Wait | `LockBehavior.wait` | Wait until the lock becomes available. This is the default. |
96+
| No wait | `LockBehavior.noWait` | Throw an exception immediately if any row is already locked. |
97+
| Skip locked | `LockBehavior.skipLocked` | Skip rows that are currently locked and return only the unlocked rows. |
98+
99+
```dart
100+
await session.db.transaction((transaction) async {
101+
var companies = await Company.db.find(
102+
session,
103+
where: (t) => t.id < 100,
104+
lockMode: LockMode.forUpdate,
105+
lockBehavior: LockBehavior.skipLocked,
106+
transaction: transaction,
107+
);
108+
109+
// Only unlocked rows are returned.
110+
});
111+
```
112+
113+
:::info
114+
`LockBehavior.skipLocked` is particularly useful for implementing job queues or work distribution, where multiple workers can each grab unlocked rows without waiting on each other.
115+
:::

0 commit comments

Comments
 (0)