You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/pages/features/transactions.mdx
+255-2Lines changed: 255 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,213 @@ title: Transactions
4
4
5
5
import { Alert } from'/components/alert.tsx'
6
6
7
-
To execute a transaction with node-postgres you simply execute `BEGIN / COMMIT / ROLLBACK` queries yourself through a client. Because node-postgres strives to be low level and un-opinionated, it doesn't provide any higher level abstractions specifically around transactions.
7
+
PostgreSQL transactions ensure that a series of database operations either all succeed or all fail together, maintaining data consistency. Node-postgres provides two approaches for handling transactions: manual transaction management and the very slightly higher-level `pg-transaction` module.
8
+
9
+
## pg-transaction Module
10
+
11
+
The `pg-transaction` module provides a tiny level of abstraction for handling transactions, automatically managing `BEGIN`, `COMMIT`, and `ROLLBACK` operations while ensuring proper client lifecycle management.
12
+
13
+
The motivation for this module was I pretty much write the same exact thing in every project I start. Sounds like a good thing to just publish widely.
14
+
15
+
### Installation
16
+
17
+
The `pg-transaction` module is included as part of the node-postgres monorepo:
18
+
19
+
```bash
20
+
npm install pg-transaction
21
+
```
22
+
23
+
### Basic Usage
24
+
25
+
The `transaction` function accepts either a `Client` or `Pool` instance and a callback function:
'INSERT INTO user_profiles(user_id, bio) VALUES($1, $2)',
88
+
[userResult.rows[0].id, 'Software developer']
89
+
)
90
+
91
+
// Return the user ID
92
+
returnuserResult.rows[0].id
93
+
})
94
+
95
+
console.log('Created user with ID:', userId)
96
+
} catch (error) {
97
+
console.error('Transaction failed:', error)
98
+
// All changes have been automatically rolled back
99
+
}
100
+
```
101
+
102
+
#### With Client
103
+
104
+
```js
105
+
import { Client } from'pg'
106
+
import { transaction } from'pg-transaction'
107
+
108
+
constclient=newClient()
109
+
awaitclient.connect()
110
+
111
+
try {
112
+
awaittransaction(client, async (client) => {
113
+
awaitclient.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [1])
114
+
awaitclient.query('UPDATE accounts SET balance = balance + 100 WHERE id = $1', [2])
115
+
})
116
+
console.log('Transfer completed successfully')
117
+
} catch (error) {
118
+
console.error('Transfer failed:', error)
119
+
} finally {
120
+
awaitclient.end()
121
+
}
122
+
```
123
+
124
+
#### Binding for Reuse
125
+
126
+
You can bind the transaction function to a specific pool or client for convenient reuse. I usually do this as a module level singleton I export after I define my pool.
127
+
128
+
```js
129
+
import { Pool } from'pg'
130
+
import { transaction } from'pg-transaction'
131
+
132
+
constpool=newPool()
133
+
consttxn=transaction.bind(null, pool)
134
+
135
+
// Now you can use txn directly
136
+
awaittxn(async (client) => {
137
+
awaitclient.query('INSERT INTO logs(message) VALUES($1)', ['Operation 1'])
138
+
})
139
+
140
+
awaittxn(async (client) => {
141
+
awaitclient.query('INSERT INTO logs(message) VALUES($1)', ['Operation 2'])
142
+
})
143
+
```
144
+
145
+
#### Error Handling and Rollback
146
+
147
+
The transaction function automatically handles rollbacks when errors occur:
148
+
149
+
```js
150
+
import { transaction } from'pg-transaction'
151
+
152
+
try {
153
+
awaittransaction(pool, async (client) => {
154
+
awaitclient.query('INSERT INTO orders(user_id, total) VALUES($1, $2)', [userId, 100])
155
+
156
+
// This will cause the transaction to rollback
157
+
if (Math.random() >0.5) {
158
+
thrownewError('Payment processing failed')
159
+
}
160
+
161
+
awaitclient.query('UPDATE inventory SET quantity = quantity - 1 WHERE product_id = $1', [productId])
162
+
})
163
+
} catch (error) {
164
+
// The transaction has been automatically rolled back
1.**Use with Pools**: When possible, use the transaction function with a `Pool` rather than a `Client` for better resource management.
172
+
173
+
2.**Keep Transactions Short**: Minimize the time spent in transactions to reduce lock contention.
174
+
175
+
3.**Handle Errors Appropriately**: Let the transaction function handle rollbacks, but ensure your application logic handles the errors appropriately.
176
+
177
+
4.**Avoid Nested Transactions**: PostgreSQL doesn't support true nested transactions. Use savepoints if you need nested behavior.
178
+
179
+
5.**Return Values**: Use the callback's return value to pass data out of the transaction.
180
+
181
+
### Migration from Manual Transactions
182
+
183
+
If you're currently using manual transaction handling, migrating to `pg-transaction` is straightforward:
184
+
185
+
**Before (Manual):**
186
+
```js
187
+
constclient=awaitpool.connect()
188
+
try {
189
+
awaitclient.query('BEGIN')
190
+
constresult=awaitclient.query('INSERT INTO users(name) VALUES($1) RETURNING id', ['Alice'])
191
+
awaitclient.query('INSERT INTO profiles(user_id) VALUES($1)', [result.rows[0].id])
192
+
awaitclient.query('COMMIT')
193
+
returnresult.rows[0]
194
+
} catch (error) {
195
+
awaitclient.query('ROLLBACK')
196
+
throw error
197
+
} finally {
198
+
client.release()
199
+
}
200
+
```
201
+
202
+
**After (pg-transaction):**
203
+
```js
204
+
returnawaittransaction(pool, async (client) => {
205
+
constresult=awaitclient.query('INSERT INTO users(name) VALUES($1) RETURNING id', ['Alice'])
206
+
awaitclient.query('INSERT INTO profiles(user_id) VALUES($1)', [result.rows[0].id])
207
+
returnresult.rows[0]
208
+
})
209
+
```
210
+
211
+
## Manual Transaction Handling
212
+
213
+
For cases where you need more control or prefer to handle transactions manually, you can execute `BEGIN`, `COMMIT`, and `ROLLBACK` queries directly.
8
214
9
215
<Alert>
10
216
You <strong>must</strong> use the <em>same</em> client instance for all statements within a transaction. PostgreSQL
@@ -13,7 +219,8 @@ To execute a transaction with node-postgres you simply execute `BEGIN / COMMIT /
13
219
the <spanclassName="code">pool.query</span> method.
14
220
</Alert>
15
221
16
-
## Examples
222
+
223
+
### Manual Transaction Example
17
224
18
225
```js
19
226
import { Pool } from'pg'
@@ -37,3 +244,49 @@ try {
37
244
client.release()
38
245
}
39
246
```
247
+
248
+
### When to Use Manual Transactions
249
+
250
+
Consider manual transaction handling when you need:
251
+
252
+
-**Savepoints**: Creating intermediate rollback points within a transaction
253
+
-**Custom Transaction Isolation Levels**: Setting specific isolation levels
254
+
-**Complex Transaction Logic**: Conditional commits or rollbacks based on business logic
255
+
-**Performance Optimization**: Fine-grained control over transaction boundaries
256
+
257
+
### Savepoints Example
258
+
259
+
```js
260
+
constclient=awaitpool.connect()
261
+
262
+
try {
263
+
awaitclient.query('BEGIN')
264
+
265
+
// First operation
266
+
awaitclient.query('INSERT INTO orders(user_id, total) VALUES($1, $2)', [userId, total])
267
+
268
+
// Create a savepoint
269
+
awaitclient.query('SAVEPOINT order_items')
270
+
271
+
try {
272
+
// Attempt to insert order items
273
+
for (constitemof items) {
274
+
awaitclient.query('INSERT INTO order_items(order_id, product_id, quantity) VALUES($1, $2, $3)',
275
+
[orderId, item.productId, item.quantity])
276
+
}
277
+
} catch (error) {
278
+
// Rollback to savepoint, keeping the order
279
+
awaitclient.query('ROLLBACK TO SAVEPOINT order_items')
280
+
console.log('Order items failed, but order preserved')
281
+
}
282
+
283
+
awaitclient.query('COMMIT')
284
+
} catch (error) {
285
+
awaitclient.query('ROLLBACK')
286
+
throw error
287
+
} finally {
288
+
client.release()
289
+
}
290
+
```
291
+
292
+
**Recommendation**: Use `pg-transaction` for most use cases, and fall back to manual transaction handling only when you need advanced features like savepoints or custom isolation levels. Note: the number of times I've done this in production apps is _nearly_ zero.
0 commit comments