mm_mysql 2.5.6 → 2.6.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/lib/mysql.js ADDED
@@ -0,0 +1,536 @@
1
+ const mysql = require('mysql2/promise');
2
+ const { Base } = require('mm_mod');
3
+ const { DB } = require('./db');
4
+ const makeQueryError = require('./query_error');
5
+
6
+ /**
7
+ * 优化版MySQL数据库操作类
8
+ * 保持必要功能,简化过度封装,直接使用mysql2模块
9
+ * @class Mysql
10
+ * @augments Base
11
+ */
12
+ class Mysql extends Base {
13
+ /**
14
+ * 默认配置(优化版,统一使用连接池)
15
+ */
16
+ static config = {
17
+ name: 'default',
18
+ host: '127.0.0.1',
19
+ port: 3306,
20
+ user: 'root',
21
+ password: 'Asd159357',
22
+ database: '',
23
+ charset: 'utf8mb4',
24
+ timezone: '+08:00',
25
+ connect_timeout: 15000,
26
+
27
+ // 连接池优化配置(针对高并发写入场景)
28
+ connection_limit: 100, // 减少连接数,避免资源竞争
29
+ acquire_timeout: 30000, // 延长获取连接超时时间
30
+ wait_for_connections: true, // 保持连接等待机制
31
+ idle_timeout: 300000, // 延长空闲连接超时时间(5分钟)
32
+ max_idle: 30, // 减少最大空闲连接数
33
+ queue_timeout: 60000, // 队列超时时间
34
+ };
35
+
36
+ /**
37
+ * 构造函数(统一使用连接池)
38
+ * @param {object} config - 配置对象
39
+ */
40
+ constructor(config = {}) {
41
+ super(config);
42
+ this._pool = null;
43
+ // 移除单连接支持,统一使用连接池
44
+
45
+ // 错误信息(注意:同 scope 下为单例共享,并发时以最后一次写入为准)
46
+ this.error = null;
47
+ // 最近执行的SQL语句
48
+ this.sql = '';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 获取MySQL2支持的配置参数
54
+ * 根据连接类型(连接池或单连接)过滤参数,避免警告
55
+ * @param {boolean} is_pool - 是否为连接池配置
56
+ * @returns {object} 过滤后的配置对象
57
+ */
58
+ Mysql.prototype._getValidConfig = function (is_pool = false) {
59
+ // 基础连接参数(适用于所有连接类型)
60
+ let base_map = {
61
+ 'host': 'host',
62
+ 'port': 'port',
63
+ 'user': 'user',
64
+ 'password': 'password',
65
+ 'database': 'database',
66
+ 'charset': 'charset',
67
+ 'timezone': 'timezone',
68
+ 'connect_timeout': 'connectTimeout'
69
+ };
70
+
71
+ // 连接池专用参数
72
+ let pool_map = {
73
+ 'connection_limit': 'connectionLimit',
74
+ 'wait_for_connections': 'waitForConnections',
75
+ 'idle_timeout': 'idleTimeout',
76
+ 'max_idle': 'maxIdle'
77
+ };
78
+
79
+ let config = {};
80
+
81
+ // 添加基础参数
82
+ for (let key in base_map) {
83
+ if (this.config[key] !== undefined) {
84
+ config[base_map[key]] = this.config[key];
85
+ }
86
+ }
87
+
88
+ // 如果是连接池,添加连接池专用参数
89
+ if (is_pool) {
90
+ for (let key in pool_map) {
91
+ if (this.config[key] !== undefined) {
92
+ config[pool_map[key]] = this.config[key];
93
+ }
94
+ }
95
+ }
96
+ // 如果是单连接,确保不包含任何连接池专用参数
97
+ else {
98
+ // 移除所有连接池专用参数,避免mysql2内部警告
99
+ for (let key in pool_map) {
100
+ if (pool_map[key] in config) {
101
+ delete config[pool_map[key]];
102
+ }
103
+ }
104
+ }
105
+
106
+ return config;
107
+ };
108
+
109
+ /**
110
+ * 打开数据库连接(统一使用连接池)
111
+ * @returns {Promise<boolean>}
112
+ * @throws {TypeError} 当配置参数无效时
113
+ */
114
+ Mysql.prototype.open = async function () {
115
+ // 参数校验
116
+ if (!this.config || typeof this.config !== 'object') {
117
+ throw new TypeError('config must be object');
118
+ }
119
+
120
+ try {
121
+ // 统一使用连接池模式
122
+ let valid_config = this._getValidConfig(true);
123
+ this._pool = mysql.createPool(valid_config);
124
+
125
+ // 测试连接池连接
126
+ let conn = await this._pool.getConnection();
127
+ conn.release();
128
+ return true;
129
+ } catch (error) {
130
+ this.log('error', '数据库连接池创建失败', error);
131
+ return false;
132
+ }
133
+ };
134
+
135
+ /**
136
+ * 关闭数据库连接(统一使用连接池)
137
+ * @returns {Promise<boolean>}
138
+ */
139
+ Mysql.prototype.close = async function () {
140
+ if (!this._pool) {
141
+ return true;
142
+ }
143
+ try {
144
+ await this._pool.end();
145
+ this._pool = null;
146
+ this.log('info', '数据库连接池已关闭');
147
+ return true;
148
+ } catch (error) {
149
+ this.log('error', '关闭连接池失败', error);
150
+ return false;
151
+ }
152
+ };
153
+
154
+ /**
155
+ * 获取数据库连接(统一使用连接池)
156
+ * @returns {Promise<object>}
157
+ * @throws {TypeError} 当连接池不存在时
158
+ */
159
+ Mysql.prototype.getConn = async function () {
160
+ // 连接池不存在则创建
161
+ if (!this._pool) {
162
+ await this.open();
163
+ }
164
+ // 连接池可能已被外部关闭,检测并重建
165
+ if (this._pool._closed) {
166
+ await this.open();
167
+ }
168
+
169
+ return await this._pool.getConnection();
170
+ };
171
+
172
+ /**
173
+ * 执行SQL查询
174
+ * @param {string} sql - SQL语句
175
+ * @param {Array} params - 参数数组
176
+ * @returns {Promise<object>}
177
+ * @throws {TypeError} 当sql参数无效时
178
+ */
179
+ Mysql.prototype.run = async function (sql, params = []) {
180
+ // 参数校验
181
+ if (typeof sql !== 'string' || sql.trim() === '') {
182
+ throw new TypeError('sql must be non-empty string');
183
+ }
184
+ if (!Array.isArray(params)) {
185
+ throw new TypeError('params must be array');
186
+ }
187
+
188
+ this.error = null;
189
+ this.sql = '';
190
+ let conn = null;
191
+ let ret = [];
192
+ try {
193
+ conn = await this.getConn();
194
+ this.sql = sql;
195
+ let [rows] = await conn.query(sql, params);
196
+ ret = rows;
197
+ } catch (err) {
198
+ // D-12:查询失败不能吞成空数组——「没数据」和「查询炸了」在业务层必须可区分
199
+ this.sql = err.sql || sql;
200
+ this.error = {
201
+ code: err.errno || err.code,
202
+ message: err.sqlMessage || err.message
203
+ };
204
+ throw makeQueryError(err, sql, params);
205
+ }
206
+ finally {
207
+ await releaseConn(conn);
208
+ }
209
+ return ret;
210
+ };
211
+
212
+
213
+ /**
214
+ * 执行SQL语句(优化版,支持预处理语句缓存)
215
+ * @param {string} sql - SQL语句
216
+ * @param {Array} params - 参数数组
217
+ * @returns {Promise<object>}
218
+ * @throws {TypeError} 当sql参数无效时
219
+ */
220
+ Mysql.prototype.exec = async function (sql, params = []) {
221
+ // 参数校验
222
+ if (typeof sql !== 'string' || sql.trim() === '') {
223
+ throw new TypeError('sql must be non-empty string');
224
+ }
225
+ if (!Array.isArray(params)) {
226
+ throw new TypeError('params must be array');
227
+ }
228
+ this.error = null;
229
+ this.sql = '';
230
+ let conn = null;
231
+ let ret = 0;
232
+ try {
233
+ conn = await this.getConn();
234
+ this.sql = sql;
235
+ let [result] = await conn.execute(sql, params);
236
+ ret = result.insertId || result.affectedRows || result.changedRows || 1;
237
+ } catch (error) {
238
+ // 写操作失败同样上浮:返回 0 会被当成「没有行受影响」正常结果
239
+ this.sql = error.sql || sql;
240
+ this.error = {
241
+ code: error.errno || error.code,
242
+ message: error.sqlMessage || error.message
243
+ };
244
+ throw makeQueryError(error, sql, params);
245
+ }
246
+ finally {
247
+ await releaseConn(conn);
248
+ }
249
+ return ret;
250
+ };
251
+
252
+ /**
253
+ * 执行多语句SQL脚本(专用于导入)
254
+ * 通过独立连接执行,不影响连接池默认配置
255
+ * @param {string} sql - SQL脚本内容,可含多条以分号分隔的语句
256
+ * @returns {Promise<object>} 执行结果统计对象
257
+ * @throws {TypeError} 当sql参数无效时
258
+ */
259
+ Mysql.prototype.runSql = async function (sql) {
260
+ // 参数校验
261
+ if (typeof sql !== 'string' || sql.trim() === '') {
262
+ throw new TypeError('sql must be non-empty string');
263
+ }
264
+
265
+ let conn = null;
266
+ let ret = { success: false, statements: 0, affected_rows: 0, error: null };
267
+ try {
268
+ // 创建专用连接,仅导入连接开启多语句支持
269
+ let valid_config = this._getValidConfig(false);
270
+ valid_config.multipleStatements = true;
271
+ conn = await mysql.createConnection(valid_config);
272
+
273
+ // 执行整段SQL脚本
274
+ let [results] = await conn.query(sql);
275
+ // 统计语句数和受影响行数
276
+ if (Array.isArray(results)) {
277
+ ret.statements = results.length;
278
+ for (let item of results) {
279
+ ret.affected_rows += item.affectedRows || 0;
280
+ }
281
+ } else {
282
+ ret.statements = 1;
283
+ ret.affected_rows = results.affectedRows || 0;
284
+ }
285
+ ret.success = true;
286
+ } catch (error) {
287
+ this.error = {
288
+ code: error.errno,
289
+ message: error.sqlMessage || error.message
290
+ };
291
+ this.sql = error.sql || sql;
292
+ ret.error = this.error.message;
293
+ } finally {
294
+ if (conn) {
295
+ try {
296
+ await conn.end();
297
+ } catch (end_error) {
298
+ this.log('error', '关闭导入连接失败', end_error);
299
+ }
300
+ }
301
+ }
302
+ return ret;
303
+ };
304
+
305
+ /**
306
+ * 开始事务(优化版)
307
+ * @returns {Promise<object>} 事务连接对象
308
+ */
309
+ Mysql.prototype.beginTransaction = async function () {
310
+ let conn = await this.getConn();
311
+
312
+ try {
313
+ await conn.beginTransaction();
314
+
315
+ let ended = false;
316
+
317
+ return {
318
+ connection: conn,
319
+ commit: async () => {
320
+ if (ended) return;
321
+ await conn.commit();
322
+ ended = true;
323
+ await releaseConn(conn);
324
+ },
325
+ rollback: async () => {
326
+ if (ended) return;
327
+ await conn.rollback();
328
+ ended = true;
329
+ await releaseConn(conn);
330
+ },
331
+ _is_ended: () => ended
332
+ };
333
+ } catch (error) {
334
+ // 如果事务开始失败,确保连接被释放
335
+ await releaseConn(conn);
336
+ this.log('error', '事务开始失败', error);
337
+ throw error;
338
+ }
339
+ };
340
+
341
+ /**
342
+ * 在事务中执行多个操作(优化版)
343
+ * @param {Function} callback - 包含事务操作的回调函数
344
+ * @returns {Promise<*>} 回调函数的返回值
345
+ * @throws {TypeError} 当callback参数无效时
346
+ */
347
+ Mysql.prototype.transaction = async function (callback) {
348
+ if (typeof callback !== 'function') {
349
+ throw new TypeError('callback must be function');
350
+ }
351
+
352
+ let transaction = await this.beginTransaction();
353
+
354
+ try {
355
+ let result = await callback(transaction);
356
+
357
+ // 如果事务未结束,自动提交
358
+ if (!transaction._is_ended()) {
359
+ await transaction.commit();
360
+ }
361
+
362
+ return result;
363
+ } catch (error) {
364
+ // 如果事务未结束,自动回滚
365
+ if (!transaction._is_ended()) {
366
+ await transaction.rollback().catch(() => {
367
+ // 简化错误处理,避免频繁日志
368
+ });
369
+ }
370
+
371
+ throw error;
372
+ }
373
+ };
374
+
375
+ /**
376
+ * 读取整张表的数据(改进版)
377
+ * @param {string} table - 表名
378
+ * @param {object} condition - 查询条件
379
+ * @param {object} options - 选项(orderBy, limit, offset等)
380
+ * @returns {Promise<Array>}
381
+ * @throws {TypeError} 当table参数无效时
382
+ */
383
+ Mysql.prototype.read = async function (table, condition = {}, options = {}) {
384
+ // 参数校验
385
+ if (typeof table !== 'string' || table.trim() === '') {
386
+ throw new TypeError('table must be non-empty string');
387
+ }
388
+ if (condition && typeof condition !== 'object') {
389
+ throw new TypeError('condition must be object');
390
+ }
391
+ if (options && typeof options !== 'object') {
392
+ throw new TypeError('options must be object');
393
+ }
394
+
395
+ try {
396
+ let sql = `SELECT * FROM ${table}`;
397
+ let params = [];
398
+
399
+ // 处理条件
400
+ if (Object.keys(condition).length > 0) {
401
+ let whereClauses = [];
402
+ for (let [field, value] of Object.entries(condition)) {
403
+ whereClauses.push(`${field} = ?`);
404
+ params.push(value);
405
+ }
406
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
407
+ }
408
+
409
+ // 处理排序
410
+ if (options.orderBy) {
411
+ sql += ` ORDER BY ${options.orderBy}`;
412
+ }
413
+
414
+ // 处理分页
415
+ if (options.limit) {
416
+ sql += ` LIMIT ${options.limit}`;
417
+ }
418
+
419
+ if (options.offset) {
420
+ sql += ` OFFSET ${options.offset}`;
421
+ }
422
+
423
+ // 执行查询
424
+ return await this.run(sql, params);
425
+ } catch (error) {
426
+ this.log('error', '查询失败', error);
427
+ throw error; // 重新抛出错误,让调用方知道查询失败
428
+ }
429
+ };
430
+
431
+ /**
432
+ * 获取连接池状态信息
433
+ * @returns {object} 连接池状态对象
434
+ */
435
+ Mysql.prototype.getPoolStats = function () {
436
+ if (!this._pool) {
437
+ return {
438
+ status: 'not_initialized',
439
+ message: '连接池未初始化'
440
+ };
441
+ }
442
+
443
+ return {
444
+ status: 'healthy'
445
+ };
446
+ };
447
+
448
+ /**
449
+ * 健康检查
450
+ * @returns {Promise<object>} 健康检查结果
451
+ */
452
+ Mysql.prototype.healthCheck = async function () {
453
+ try {
454
+ if (!this._pool) {
455
+ return {
456
+ status: 'error',
457
+ message: '数据库连接未初始化'
458
+ };
459
+ }
460
+
461
+ // 执行简单的查询来检查连接状态
462
+ let result = await this.run('SELECT 1 as health_check');
463
+
464
+ if (result && result.length > 0 && result[0].health_check === 1) {
465
+ return {
466
+ status: 'healthy',
467
+ message: '数据库连接正常',
468
+ ...this.getPoolStats()
469
+ };
470
+ } else {
471
+ return {
472
+ status: 'error',
473
+ message: '健康检查查询失败'
474
+ };
475
+ }
476
+ } catch (error) {
477
+ this.log('error', '健康检查失败', error);
478
+ return {
479
+ status: 'error',
480
+ message: `健康检查失败: ${error.message}`
481
+ };
482
+ }
483
+ };
484
+
485
+ /**
486
+ * 获取数据库管理器(保持兼容性)
487
+ * @returns {object} DB实例
488
+ */
489
+ Mysql.prototype.db = function () {
490
+ return new DB(this);
491
+ };
492
+
493
+ /**
494
+ * 获取表管理器(语法糖,等同于 db().new(name, key))
495
+ * @param {string} name 表名
496
+ * @param {string} key 主键字段名
497
+ * @returns {object} DB表管理器实例
498
+ */
499
+ Mysql.prototype.table = function (name, key) {
500
+ if (typeof name !== 'string' || name.trim() === '') {
501
+ throw new TypeError('name must be non-empty string');
502
+ }
503
+ var db = this.db();
504
+ db.table = name;
505
+ if (key) {
506
+ db.key = key;
507
+ }
508
+ return db;
509
+ };
510
+
511
+ /**
512
+ * 初始化钩子(由 Lifecycle.init 模板方法调用,保证状态机流转到 inited)
513
+ * @returns {Promise<void>}
514
+ */
515
+ Mysql.prototype.onInit = async function () {
516
+ await this.open();
517
+ };
518
+
519
+ /**
520
+ * 安全释放数据库连接
521
+ * @param {object} conn - 数据库连接对象
522
+ * @returns {Promise<void>}
523
+ */
524
+ async function releaseConn(conn) {
525
+ if (!conn) return;
526
+
527
+ try {
528
+ if (typeof conn.release === 'function') {
529
+ await conn.release();
530
+ }
531
+ } catch (release_err) {
532
+ console.error('释放连接失败', release_err);
533
+ }
534
+ };
535
+
536
+ module.exports = Mysql;
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 构造带上下文的数据层错误
5
+ *
6
+ * 独立函数而非实例方法:DB 包装层以 `super(sqlite.run, sqlite.exec)` 传递未绑定的方法引用,
7
+ * 调用时 this 指向 DB 实例而不是 SQLite 实例,任何 this.xxx 辅助方法都会失效。
8
+ * 用模块级函数可同时覆盖「SQLite 直接被调用」与「经 DB 包装调用」两条路径。
9
+ *
10
+ * @param {Error} cause 驱动抛出的原始异常
11
+ * @param {string} sql 出错的 SQL
12
+ * @param {Array} params 绑定参数
13
+ * @returns {Error} 携带 code / sql / params / cause 的错误对象
14
+ */
15
+ module.exports = function makeQueryError(cause, sql, params) {
16
+ const error = new Error(`SQL执行失败: ${cause.message || cause}`);
17
+ error.code = cause.code || cause.errno;
18
+ error.sql = sql;
19
+ error.params = params;
20
+ error.cause = cause;
21
+ return error;
22
+ };
@@ -4,7 +4,8 @@
4
4
  * @version 1.2
5
5
  */
6
6
  const SqlString = require('sqlstring');
7
- const { Base } = require('mm_expand');
7
+ const { Base } = require('mm_mod');
8
+ const { loadText, hasFile } = require('mm_helper');
8
9
 
9
10
  function escape(value, stringifyObjects, timeZone) {
10
11
  return SqlString.escape(value, stringifyObjects, timeZone);
@@ -236,6 +237,10 @@ Sql.prototype.delSql = function (where) {
236
237
  * @returns {Promise | object} 执行结果
237
238
  */
238
239
  Sql.prototype.setSql = function (where, set) {
240
+ // 空 SET 会拼出非法 SQL,按“无变更”处理
241
+ if (!set) {
242
+ return 0;
243
+ }
239
244
  // 使用直接拼接的方式,避免模板替换错误
240
245
  var sql = `UPDATE \`${this.table}\` SET ${set} WHERE ${where};`;
241
246
  return this.exec(sql);
@@ -645,6 +650,12 @@ Sql.prototype.set = async function (query, body, like) {
645
650
  if (!this.table || !body || typeof body !== 'object') {
646
651
  throw new Error('表名或数据未设置');
647
652
  }
653
+ // 待更新字段全为 undefined/null 时 toSet 会得到空串,
654
+ // 此时执行会产生 `UPDATE t SET WHERE ...` 的非法 SQL,直接按“无变更”返回 0
655
+ if (!this.toSet(body)) {
656
+ this.sql = '';
657
+ return 0;
658
+ }
648
659
  let sql = this.toSetSql(query, body, like);
649
660
  this.sql = sql;
650
661
  let bl = await this.exec(sql);
@@ -1233,7 +1244,6 @@ Sql.prototype.tplBody = function (param_dt, sql_dt) {
1233
1244
  * @returns {Promise<boolean>} 是否加载成功
1234
1245
  */
1235
1246
  Sql.prototype.load = async function (file) {
1236
-
1237
1247
  await this._logLoadStart(file);
1238
1248
  await this._validateFile(file);
1239
1249
  let sql_stmts = await this._readAndSplitSql(file);
@@ -1259,7 +1269,7 @@ Sql.prototype._logLoadStart = async function (file) {
1259
1269
  * @returns {Promise<void>}
1260
1270
  */
1261
1271
  Sql.prototype._validateFile = async function (file) {
1262
- if (!file.hasFile()) {
1272
+ if (!hasFile(file)) {
1263
1273
  throw new Error(`SQL文件不存在: ${file}`);
1264
1274
  }
1265
1275
  };
@@ -1270,7 +1280,7 @@ Sql.prototype._validateFile = async function (file) {
1270
1280
  * @returns {Promise<string[]>} SQL语句数组
1271
1281
  */
1272
1282
  Sql.prototype._readAndSplitSql = async function (file) {
1273
- let sql_content = await $.file.readText(file);
1283
+ let sql_content = loadText(file);
1274
1284
  if (!sql_content || sql_content.trim() === '') {
1275
1285
  throw new Error(`SQL文件内容为空: ${file}`);
1276
1286
  }
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ /** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
5
+ module.exports = new Proxy({}, {
6
+ get(_t, key) {
7
+ if (typeof key !== 'string') return undefined;
8
+ if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
9
+ return require('./' + key);
10
+ },
11
+ has(_t, key) {
12
+ return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
13
+ },
14
+ ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
15
+ getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
16
+ });