@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 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
- level = 'READ UNCOMMITTED' | 'RU'
194
- | 'READ COMMITTED' | 'RC'
195
- | 'REPEATABLE READ' | 'RR'
196
- | 'SERIALIZABLE' | 'S'
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
- // insert user info
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
- // insert student info
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
@@ -9,7 +9,7 @@ const app = new App({
9
9
  name: 'MySQL ORM CLI',
10
10
  desc: 'migrate, model, seed, etc.',
11
11
  bin: 'orm-mysql',
12
- version: '0.11.12',
12
+ version: '0.12.0',
13
13
  commands_dir: path.join(__dirname, '../commands'),
14
14
  });
15
15
 
@@ -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.11.12",
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 = {