@axiosleo/orm-mysql 0.11.12 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -7
- package/bin/orm-mysql.js +1 -1
- package/docker-compose.yml +18 -0
- package/examples/transaction_example.js +228 -0
- package/index.d.ts +26 -0
- package/package.json +4 -1
- package/src/operator.js +115 -0
package/README.md
CHANGED
|
@@ -177,6 +177,53 @@ Hook.post(async (options, result) => {
|
|
|
177
177
|
|
|
178
178
|
### Transaction
|
|
179
179
|
|
|
180
|
+
#### Method 1: Using Connection Pool (Recommended)
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
const mysql = require("mysql2");
|
|
184
|
+
const { QueryHandler } = require("@axiosleo/orm-mysql");
|
|
185
|
+
|
|
186
|
+
// Create connection pool
|
|
187
|
+
const pool = mysql.createPool({
|
|
188
|
+
host: process.env.MYSQL_HOST,
|
|
189
|
+
port: process.env.MYSQL_PORT,
|
|
190
|
+
user: process.env.MYSQL_USER,
|
|
191
|
+
password: process.env.MYSQL_PASS,
|
|
192
|
+
database: process.env.MYSQL_DB,
|
|
193
|
+
connectionLimit: 10
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const handler = new QueryHandler(pool);
|
|
197
|
+
|
|
198
|
+
// Begin transaction - automatically gets a connection from pool
|
|
199
|
+
const transaction = await handler.beginTransaction({
|
|
200
|
+
level: "RC" // READ COMMITTED
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Insert user info
|
|
205
|
+
let row = await transaction.table("users").insert({
|
|
206
|
+
name: "Joe",
|
|
207
|
+
age: 18,
|
|
208
|
+
});
|
|
209
|
+
const lastInsertId = row.insertId;
|
|
210
|
+
|
|
211
|
+
// Insert student info
|
|
212
|
+
await transaction.table("students").insert({
|
|
213
|
+
user_id: lastInsertId,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Commit transaction - connection automatically released back to pool
|
|
217
|
+
await transaction.commit();
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Rollback transaction - connection automatically released back to pool
|
|
220
|
+
await transaction.rollback();
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Method 2: Using TransactionHandler Directly
|
|
226
|
+
|
|
180
227
|
```javascript
|
|
181
228
|
const { TransactionHandler, createPromiseClient } = require("@axiosleo/orm-mysql");
|
|
182
229
|
|
|
@@ -190,25 +237,25 @@ const conn = await createPromiseClient({
|
|
|
190
237
|
|
|
191
238
|
const transaction = new TransactionHandler(conn, {
|
|
192
239
|
/*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
240
|
+
Transaction Isolation Levels:
|
|
241
|
+
- 'READ UNCOMMITTED' | 'RU' : Lowest isolation, may read dirty data
|
|
242
|
+
- 'READ COMMITTED' | 'RC' : Prevents dirty reads
|
|
243
|
+
- 'REPEATABLE READ' | 'RR' : MySQL default, prevents non-repeatable reads
|
|
244
|
+
- 'SERIALIZABLE' | 'S' : Highest isolation, full serialization
|
|
197
245
|
*/
|
|
198
246
|
level: "SERIALIZABLE", // 'SERIALIZABLE' as default value
|
|
199
247
|
});
|
|
200
248
|
await transaction.begin();
|
|
201
249
|
|
|
202
250
|
try {
|
|
203
|
-
//
|
|
204
|
-
// will not really create a record.
|
|
251
|
+
// Insert user info
|
|
205
252
|
let row = await transaction.table("users").insert({
|
|
206
253
|
name: "Joe",
|
|
207
254
|
age: 18,
|
|
208
255
|
});
|
|
209
256
|
const lastInsertId = row[0].insertId;
|
|
210
257
|
|
|
211
|
-
//
|
|
258
|
+
// Insert student info
|
|
212
259
|
await transaction.table("students").insert({
|
|
213
260
|
user_id: lastInsertId,
|
|
214
261
|
});
|
|
@@ -219,6 +266,95 @@ try {
|
|
|
219
266
|
}
|
|
220
267
|
```
|
|
221
268
|
|
|
269
|
+
#### Row Locking with FOR UPDATE
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
const transaction = await handler.beginTransaction({ level: "RC" });
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
// Lock rows for update
|
|
276
|
+
const product = await transaction.table("products")
|
|
277
|
+
.where("sku", "LAPTOP-001")
|
|
278
|
+
.append("FOR UPDATE") // Lock the row
|
|
279
|
+
.find();
|
|
280
|
+
|
|
281
|
+
if (product.stock < 1) {
|
|
282
|
+
throw new Error("Out of stock");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update stock
|
|
286
|
+
await transaction.table("products")
|
|
287
|
+
.where("sku", "LAPTOP-001")
|
|
288
|
+
.update({ stock: product.stock - 1 });
|
|
289
|
+
|
|
290
|
+
// Create order
|
|
291
|
+
await transaction.table("orders").insert({
|
|
292
|
+
product_id: product.id,
|
|
293
|
+
quantity: 1,
|
|
294
|
+
total: product.price
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await transaction.commit();
|
|
298
|
+
} catch (e) {
|
|
299
|
+
await transaction.rollback();
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Concurrent Transactions
|
|
305
|
+
|
|
306
|
+
When using a connection pool, multiple transactions can run concurrently without blocking each other:
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
const pool = mysql.createPool({ /* ... */ });
|
|
310
|
+
const handler = new QueryHandler(pool);
|
|
311
|
+
|
|
312
|
+
// Run 3 transactions concurrently
|
|
313
|
+
await Promise.all([
|
|
314
|
+
(async () => {
|
|
315
|
+
const tx = await handler.beginTransaction();
|
|
316
|
+
try {
|
|
317
|
+
await tx.table("users").insert({ name: "User1" });
|
|
318
|
+
await tx.commit();
|
|
319
|
+
} catch (err) {
|
|
320
|
+
await tx.rollback();
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
})(),
|
|
324
|
+
|
|
325
|
+
(async () => {
|
|
326
|
+
const tx = await handler.beginTransaction();
|
|
327
|
+
try {
|
|
328
|
+
await tx.table("users").insert({ name: "User2" });
|
|
329
|
+
await tx.commit();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
await tx.rollback();
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
})(),
|
|
335
|
+
|
|
336
|
+
(async () => {
|
|
337
|
+
const tx = await handler.beginTransaction();
|
|
338
|
+
try {
|
|
339
|
+
await tx.table("users").insert({ name: "User3" });
|
|
340
|
+
await tx.commit();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
await tx.rollback();
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
})()
|
|
346
|
+
]);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### Best Practices
|
|
350
|
+
|
|
351
|
+
1. **Always use connection pools in production** - Prevents connection exhaustion and enables concurrent transactions
|
|
352
|
+
2. **Choose appropriate isolation level** - Balance between consistency and performance
|
|
353
|
+
3. **Use try-catch-finally** - Ensure transactions are always committed or rolled back
|
|
354
|
+
4. **Keep transactions short** - Avoid long-running operations inside transactions
|
|
355
|
+
5. **Use row locking when needed** - `FOR UPDATE` prevents concurrent modifications
|
|
356
|
+
6. **Handle errors properly** - Always rollback on errors to maintain data consistency
|
|
357
|
+
|
|
222
358
|
### Migration
|
|
223
359
|
|
|
224
360
|
> [Migration examples](./examples/migration/).
|
package/bin/orm-mysql.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: '3'
|
|
2
|
+
# Settings and configurations that are common for all containers
|
|
3
|
+
services:
|
|
4
|
+
mysql:
|
|
5
|
+
image: 'mysql:8.0'
|
|
6
|
+
container_name: "orm-feature-tests-mysql"
|
|
7
|
+
command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
|
8
|
+
environment:
|
|
9
|
+
MYSQL_ROOT_PASSWORD: '3AQqZTfmww=Ftj'
|
|
10
|
+
MYSQL_DATABASE: 'feature_tests'
|
|
11
|
+
restart: 'always'
|
|
12
|
+
ports:
|
|
13
|
+
- "3306:3306"
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p3AQqZTfmww=Ftj"]
|
|
16
|
+
interval: 5s
|
|
17
|
+
timeout: 3s
|
|
18
|
+
retries: 10
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 事务使用示例
|
|
6
|
+
*
|
|
7
|
+
* 这个示例展示了如何正确使用事务,避免连接阻塞问题
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const mysql = require('mysql2/promise');
|
|
11
|
+
const { QueryHandler } = require('../src/operator');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 示例 1: 使用连接池(推荐)
|
|
15
|
+
* 优点:自动从池中获取新连接,不会阻塞其他操作
|
|
16
|
+
*/
|
|
17
|
+
async function exampleWithPool() {
|
|
18
|
+
const pool = mysql.createPool({
|
|
19
|
+
host: 'localhost',
|
|
20
|
+
port: 3306,
|
|
21
|
+
user: 'root',
|
|
22
|
+
password: 'password',
|
|
23
|
+
database: 'test_db',
|
|
24
|
+
connectionLimit: 10
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const queryHandler = new QueryHandler(pool);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 方式 1: 使用 QueryHandler.beginTransaction()(推荐)
|
|
31
|
+
// 这会自动从连接池获取一个新连接用于事务
|
|
32
|
+
const transaction = await queryHandler.beginTransaction({ level: 'RC' });
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// 执行事务操作
|
|
36
|
+
await transaction.table('users').insert({ name: 'John', age: 30 });
|
|
37
|
+
await transaction.table('orders').insert({ user_id: 1, total: 100 });
|
|
38
|
+
|
|
39
|
+
// 提交事务(会自动释放连接回池)
|
|
40
|
+
await transaction.commit();
|
|
41
|
+
console.log('Transaction committed successfully');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// 回滚事务(会自动释放连接回池)
|
|
44
|
+
await transaction.rollback();
|
|
45
|
+
console.error('Transaction rolled back:', err.message);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 同时,其他操作不会被阻塞
|
|
50
|
+
const users = await queryHandler.table('users').select();
|
|
51
|
+
console.log('Users:', users);
|
|
52
|
+
|
|
53
|
+
} finally {
|
|
54
|
+
await pool.end();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 示例 2: 使用单一连接(不推荐用于生产环境)
|
|
60
|
+
* 注意:事务执行期间会阻塞该连接的其他操作
|
|
61
|
+
*/
|
|
62
|
+
async function exampleWithSingleConnection() {
|
|
63
|
+
const connection = await mysql.createConnection({
|
|
64
|
+
host: 'localhost',
|
|
65
|
+
port: 3306,
|
|
66
|
+
user: 'root',
|
|
67
|
+
password: 'password',
|
|
68
|
+
database: 'test_db'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const queryHandler = new QueryHandler(connection);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// 使用单一连接创建事务
|
|
75
|
+
const transaction = await queryHandler.beginTransaction({ level: 'RR' });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await transaction.table('users').insert({ name: 'Jane', age: 25 });
|
|
79
|
+
await transaction.commit();
|
|
80
|
+
console.log('Transaction committed');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
await transaction.rollback();
|
|
83
|
+
console.error('Transaction rolled back:', err);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} finally {
|
|
88
|
+
await connection.end();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 示例 3: 直接使用 TransactionHandler
|
|
94
|
+
* 适用于需要更多控制的场景
|
|
95
|
+
*/
|
|
96
|
+
async function exampleWithTransactionHandler() {
|
|
97
|
+
const { TransactionHandler } = require('../src/transaction');
|
|
98
|
+
|
|
99
|
+
const connection = await mysql.createConnection({
|
|
100
|
+
host: 'localhost',
|
|
101
|
+
port: 3306,
|
|
102
|
+
user: 'root',
|
|
103
|
+
password: 'password',
|
|
104
|
+
database: 'test_db'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const transaction = new TransactionHandler(connection, { level: 'SERIALIZABLE' });
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await transaction.begin();
|
|
111
|
+
|
|
112
|
+
// 执行多个操作
|
|
113
|
+
await transaction.table('users').insert({ name: 'Bob', age: 35 });
|
|
114
|
+
await transaction.table('user_profiles').insert({ user_id: 1, bio: 'Developer' });
|
|
115
|
+
|
|
116
|
+
// 使用锁
|
|
117
|
+
const lockedRows = await transaction.table('products')
|
|
118
|
+
.where('id', [1, 2, 3], 'IN')
|
|
119
|
+
.append('FOR UPDATE')
|
|
120
|
+
.select();
|
|
121
|
+
|
|
122
|
+
console.log('Locked rows:', lockedRows);
|
|
123
|
+
|
|
124
|
+
await transaction.commit();
|
|
125
|
+
console.log('Transaction completed');
|
|
126
|
+
} catch (err) {
|
|
127
|
+
await transaction.rollback();
|
|
128
|
+
console.error('Error:', err);
|
|
129
|
+
throw err;
|
|
130
|
+
} finally {
|
|
131
|
+
await connection.end();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 示例 4: 并发事务(使用连接池)
|
|
137
|
+
* 展示多个事务可以同时执行而不相互阻塞
|
|
138
|
+
*/
|
|
139
|
+
async function exampleConcurrentTransactions() {
|
|
140
|
+
const pool = mysql.createPool({
|
|
141
|
+
host: 'localhost',
|
|
142
|
+
port: 3306,
|
|
143
|
+
user: 'root',
|
|
144
|
+
password: 'password',
|
|
145
|
+
database: 'test_db',
|
|
146
|
+
connectionLimit: 10
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const queryHandler = new QueryHandler(pool);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// 并发执行多个事务
|
|
153
|
+
const results = await Promise.all([
|
|
154
|
+
// 事务 1
|
|
155
|
+
(async () => {
|
|
156
|
+
const tx = await queryHandler.beginTransaction();
|
|
157
|
+
try {
|
|
158
|
+
await tx.table('users').insert({ name: 'User1', age: 20 });
|
|
159
|
+
await tx.commit();
|
|
160
|
+
return 'Transaction 1 completed';
|
|
161
|
+
} catch (err) {
|
|
162
|
+
await tx.rollback();
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
})(),
|
|
166
|
+
|
|
167
|
+
// 事务 2
|
|
168
|
+
(async () => {
|
|
169
|
+
const tx = await queryHandler.beginTransaction();
|
|
170
|
+
try {
|
|
171
|
+
await tx.table('users').insert({ name: 'User2', age: 21 });
|
|
172
|
+
await tx.commit();
|
|
173
|
+
return 'Transaction 2 completed';
|
|
174
|
+
} catch (err) {
|
|
175
|
+
await tx.rollback();
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
})(),
|
|
179
|
+
|
|
180
|
+
// 事务 3
|
|
181
|
+
(async () => {
|
|
182
|
+
const tx = await queryHandler.beginTransaction();
|
|
183
|
+
try {
|
|
184
|
+
await tx.table('users').insert({ name: 'User3', age: 22 });
|
|
185
|
+
await tx.commit();
|
|
186
|
+
return 'Transaction 3 completed';
|
|
187
|
+
} catch (err) {
|
|
188
|
+
await tx.rollback();
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
})()
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
console.log('All transactions completed:', results);
|
|
195
|
+
} finally {
|
|
196
|
+
await pool.end();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 最佳实践总结:
|
|
202
|
+
*
|
|
203
|
+
* 1. 生产环境推荐使用连接池(Pool)
|
|
204
|
+
* 2. 使用 QueryHandler.beginTransaction() 自动管理连接
|
|
205
|
+
* 3. 始终在 try-catch-finally 中使用事务
|
|
206
|
+
* 4. 确保调用 commit() 或 rollback()
|
|
207
|
+
* 5. 连接池会自动释放连接,无需手动管理
|
|
208
|
+
* 6. 避免在事务中执行长时间运行的操作
|
|
209
|
+
* 7. 根据需要选择合适的隔离级别:
|
|
210
|
+
* - 'RU' / 'READ UNCOMMITTED': 最低隔离级别,性能最好,可能读到脏数据
|
|
211
|
+
* - 'RC' / 'READ COMMITTED': 避免脏读
|
|
212
|
+
* - 'RR' / 'REPEATABLE READ': MySQL 默认级别,避免不可重复读
|
|
213
|
+
* - 'S' / 'SERIALIZABLE': 最高隔离级别,完全串行化,性能最差
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
// 运行示例(取消注释以运行)
|
|
217
|
+
// exampleWithPool().catch(console.error);
|
|
218
|
+
// exampleWithSingleConnection().catch(console.error);
|
|
219
|
+
// exampleWithTransactionHandler().catch(console.error);
|
|
220
|
+
// exampleConcurrentTransactions().catch(console.error);
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
exampleWithPool,
|
|
224
|
+
exampleWithSingleConnection,
|
|
225
|
+
exampleWithTransactionHandler,
|
|
226
|
+
exampleConcurrentTransactions
|
|
227
|
+
};
|
|
228
|
+
|
package/index.d.ts
CHANGED
|
@@ -412,6 +412,32 @@ export declare class QueryHandler {
|
|
|
412
412
|
* @param attrs
|
|
413
413
|
*/
|
|
414
414
|
getTableFields<T extends Object>(database: string, table: string, ...attrs: TableInfoColumn[]): Promise<T>;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Begin a transaction and return a TransactionHandler instance
|
|
418
|
+
*
|
|
419
|
+
* Note: If using a Pool, this will automatically get a new connection from the pool
|
|
420
|
+
* to avoid blocking other operations. The connection will be released back to pool
|
|
421
|
+
* after commit/rollback.
|
|
422
|
+
*
|
|
423
|
+
* @param options - Transaction options
|
|
424
|
+
* @param options.level - Transaction isolation level
|
|
425
|
+
* @param options.useNewConnection - Force create new connection from pool (default: true for Pool, false for Connection)
|
|
426
|
+
*/
|
|
427
|
+
beginTransaction(options?: {
|
|
428
|
+
level?: TransactionLevel;
|
|
429
|
+
useNewConnection?: boolean;
|
|
430
|
+
}): Promise<TransactionHandler>;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Commit current connection transaction (if using promise connection directly)
|
|
434
|
+
*/
|
|
435
|
+
commit(): Promise<void>;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Rollback current connection transaction (if using promise connection directly)
|
|
439
|
+
*/
|
|
440
|
+
rollback(): Promise<void>;
|
|
415
441
|
}
|
|
416
442
|
|
|
417
443
|
export declare class TransactionOperator extends QueryOperator {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiosleo/orm-mysql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "MySQL ORM tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mysql",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"test": "mocha --reporter spec --timeout 3000 tests/*.tests.js",
|
|
19
19
|
"test-cov": "nyc -r=lcov -r=html -r=text -r=json mocha -t 10000 -R spec tests/*.tests.js",
|
|
20
20
|
"test-one": "mocha --reporter spec --timeout 3000 ",
|
|
21
|
+
"feature-test": "node tests/transaction.ft.js",
|
|
22
|
+
"setup-feature-db": "node tests/setup-feature-db.js",
|
|
23
|
+
"feature-test:local": "docker compose up -d && sleep 10 && npm run setup-feature-db && npm run feature-test && docker compose down",
|
|
21
24
|
"ci": "npm run lint && npm run test-cov",
|
|
22
25
|
"clear": "rm -rf ./nyc_output ./coverage && rm -rf ./node_modules && npm cache clean --force"
|
|
23
26
|
},
|
package/src/operator.js
CHANGED
|
@@ -259,6 +259,121 @@ class QueryHandler {
|
|
|
259
259
|
values: [database, table]
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Begin a transaction and return a TransactionHandler instance
|
|
265
|
+
*
|
|
266
|
+
* Note: If using a Pool, this will automatically get a new connection from the pool
|
|
267
|
+
* to avoid blocking other operations. The connection will be released back to pool
|
|
268
|
+
* after commit/rollback.
|
|
269
|
+
*
|
|
270
|
+
* @param {object} options - Transaction options
|
|
271
|
+
* @param {string} options.level - Transaction isolation level (RU, RC, RR, S or full name)
|
|
272
|
+
* @param {boolean} options.useNewConnection - Force create new connection from pool (default: true for Pool, false for Connection)
|
|
273
|
+
* @returns {Promise<import('./transaction').TransactionHandler>}
|
|
274
|
+
*/
|
|
275
|
+
async beginTransaction(options = {}) {
|
|
276
|
+
const { TransactionHandler } = require('./transaction');
|
|
277
|
+
const transactionOptions = {
|
|
278
|
+
...this.options,
|
|
279
|
+
level: options.level || 'SERIALIZABLE'
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
let conn = this.conn;
|
|
283
|
+
let isPoolConnection = false;
|
|
284
|
+
|
|
285
|
+
// Check if this.conn is a Pool
|
|
286
|
+
if (this.conn && typeof this.conn.getConnection === 'function') {
|
|
287
|
+
// This is a Pool, get a new connection from pool for transaction
|
|
288
|
+
const useNewConnection = options.useNewConnection !== false; // default true for pool
|
|
289
|
+
if (useNewConnection) {
|
|
290
|
+
// Detect pool type by checking constructor name or promise() method
|
|
291
|
+
// Callback pool (mysql2): has promise() method, constructor name is "Pool"
|
|
292
|
+
// Promise pool (mysql2/promise): no promise() method, constructor name is "PromisePool"
|
|
293
|
+
const isCallbackPool = typeof this.conn.promise === 'function';
|
|
294
|
+
|
|
295
|
+
if (isCallbackPool) {
|
|
296
|
+
// Callback-based pool (mysql2) - need to wrap getConnection in Promise
|
|
297
|
+
conn = await new Promise((resolve, reject) => {
|
|
298
|
+
this.conn.getConnection((err, connection) => {
|
|
299
|
+
if (err) {
|
|
300
|
+
reject(err);
|
|
301
|
+
} else {
|
|
302
|
+
resolve(connection);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
// Convert callback connection to promise-based
|
|
307
|
+
if (conn && typeof conn.promise === 'function') {
|
|
308
|
+
conn = conn.promise();
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Promise-based pool (mysql2/promise) - getConnection() already returns a Promise
|
|
312
|
+
conn = await this.conn.getConnection();
|
|
313
|
+
}
|
|
314
|
+
isPoolConnection = true;
|
|
315
|
+
}
|
|
316
|
+
} else if (this.conn && typeof this.conn.promise === 'function') {
|
|
317
|
+
// This is a mysql2 Connection, convert to promise-based
|
|
318
|
+
conn = this.conn.promise();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const transaction = new TransactionHandler(conn, transactionOptions);
|
|
322
|
+
|
|
323
|
+
// Store pool connection flag for cleanup
|
|
324
|
+
if (isPoolConnection) {
|
|
325
|
+
transaction._poolConnection = conn;
|
|
326
|
+
transaction._originalCommit = transaction.commit.bind(transaction);
|
|
327
|
+
transaction._originalRollback = transaction.rollback.bind(transaction);
|
|
328
|
+
|
|
329
|
+
// Override commit to release connection back to pool
|
|
330
|
+
transaction.commit = async function () {
|
|
331
|
+
try {
|
|
332
|
+
await this._originalCommit();
|
|
333
|
+
} finally {
|
|
334
|
+
if (this._poolConnection && typeof this._poolConnection.release === 'function') {
|
|
335
|
+
this._poolConnection.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Override rollback to release connection back to pool
|
|
341
|
+
transaction.rollback = async function () {
|
|
342
|
+
try {
|
|
343
|
+
await this._originalRollback();
|
|
344
|
+
} finally {
|
|
345
|
+
if (this._poolConnection && typeof this._poolConnection.release === 'function') {
|
|
346
|
+
this._poolConnection.release();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await transaction.begin();
|
|
353
|
+
return transaction;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Commit current connection transaction (if using promise connection directly)
|
|
358
|
+
*/
|
|
359
|
+
async commit() {
|
|
360
|
+
if (this.conn && typeof this.conn.commit === 'function') {
|
|
361
|
+
await this.conn.commit();
|
|
362
|
+
} else {
|
|
363
|
+
throw new Error('Connection does not support commit operation');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Rollback current connection transaction (if using promise connection directly)
|
|
369
|
+
*/
|
|
370
|
+
async rollback() {
|
|
371
|
+
if (this.conn && typeof this.conn.rollback === 'function') {
|
|
372
|
+
await this.conn.rollback();
|
|
373
|
+
} else {
|
|
374
|
+
throw new Error('Connection does not support rollback operation');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
262
377
|
}
|
|
263
378
|
|
|
264
379
|
module.exports = {
|