mdbq 4.2.25__py3-none-any.whl → 4.2.27__py3-none-any.whl

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.

Potentially problematic release.


This version of mdbq might be problematic. Click here for more details.

@@ -0,0 +1,1452 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MySQL数据查询、导出、表结构获取等功能
5
+
6
+ 功能特性:
7
+ - 连接池管理,高性能
8
+ - SSL安全连接支持
9
+ - SQL注入防护
10
+ - 灵活的日志管理(控制台/文件/禁用)
11
+ - 自动资源管理,防止内存泄漏
12
+ - 支持with上下文管理器
13
+ - 多种数据格式导出(字典、DataFrame、JSON、CSV、Excel)
14
+ - 表结构和信息查询
15
+
16
+ 依赖:
17
+ pip install pymysql DBUtils pandas openpyxl
18
+
19
+ """
20
+
21
+ import json
22
+ import logging
23
+ import re
24
+ import time
25
+ import weakref
26
+ from pathlib import Path
27
+ from typing import List, Dict, Any, Optional, Union, Tuple, TypedDict, Literal
28
+ from datetime import datetime
29
+ from contextlib import contextmanager
30
+ from functools import wraps
31
+ import pymysql
32
+ from pymysql.cursors import DictCursor
33
+ from dbutils.pooled_db import PooledDB
34
+
35
+
36
+ # ==================== 自定义异常类 ====================
37
+
38
+
39
+ class MySQLQueryError(Exception):
40
+ """MySQL查询异常基类"""
41
+ pass
42
+
43
+
44
+ class ConnectionError(MySQLQueryError):
45
+ """数据库连接错误"""
46
+ pass
47
+
48
+
49
+ class QueryError(MySQLQueryError):
50
+ """查询执行错误"""
51
+ pass
52
+
53
+
54
+ class ValidationError(MySQLQueryError):
55
+ """参数验证错误"""
56
+ pass
57
+
58
+
59
+ class TransactionError(MySQLQueryError):
60
+ """事务处理错误"""
61
+ pass
62
+
63
+
64
+ # ==================== 类型定义 ====================
65
+
66
+
67
+ class TableInfo(TypedDict, total=False):
68
+ """表信息类型定义"""
69
+ table_name: str
70
+ engine: str
71
+ row_count: int
72
+ avg_row_length: int
73
+ data_length: int
74
+ index_length: int
75
+ auto_increment: Optional[int]
76
+ create_time: Optional[datetime]
77
+ update_time: Optional[datetime]
78
+ collation: str
79
+ comment: str
80
+
81
+
82
+ class FieldInfo(TypedDict):
83
+ """字段信息类型定义"""
84
+ Field: str
85
+ Type: str
86
+ Null: str
87
+ Key: str
88
+ Default: Optional[str]
89
+ Extra: str
90
+
91
+
92
+ # ==================== 工具函数 ====================
93
+
94
+
95
+ def validate_identifier(name: str, type_: str = "标识符") -> str:
96
+ """
97
+ 验证数据库标识符(表名、库名、字段名等)
98
+
99
+ 参数:
100
+ name: 标识符名称
101
+ type_: 标识符类型(用于错误提示)
102
+
103
+ 返回:
104
+ 验证通过的标识符
105
+
106
+ 异常:
107
+ ValidationError: 标识符格式不合法
108
+ """
109
+ if not name:
110
+ raise ValidationError(f"{type_}不能为空")
111
+
112
+ # 允许:字母、数字、下划线、中文、点号(用于database.table格式)
113
+ if not re.match(r'^[\w\u4e00-\u9fa5.]+$', name):
114
+ raise ValidationError(f"非法的{type_}名称: {name},只允许字母、数字、下划线和中文")
115
+
116
+ # 检查长度
117
+ if len(name) > 64:
118
+ raise ValidationError(f"{type_}名称过长(最大64字符): {name}")
119
+
120
+ return name
121
+
122
+
123
+ def mask_password(password: str) -> str:
124
+ """
125
+ 脱敏密码用于日志显示
126
+
127
+ 参数:
128
+ password: 原始密码
129
+
130
+ 返回:
131
+ 脱敏后的密码
132
+ """
133
+ if not password:
134
+ return ''
135
+ if len(password) <= 2:
136
+ return '*' * len(password)
137
+ return password[0] + '*' * (len(password) - 2) + password[-1]
138
+
139
+
140
+ def escape_identifier(identifier: str) -> str:
141
+ """
142
+ 转义SQL标识符(反引号转义)
143
+
144
+ 参数:
145
+ identifier: 标识符
146
+
147
+ 返回:
148
+ 转义后的标识符
149
+ """
150
+ return identifier.replace('`', '``')
151
+
152
+
153
+ # ==================== 装饰器 ====================
154
+
155
+
156
+ def retry_on_failure(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
157
+ """
158
+ 查询失败自动重试装饰器
159
+
160
+ 参数:
161
+ max_retries: 最大重试次数
162
+ delay: 初始延迟时间(秒)
163
+ backoff: 延迟时间倍增系数
164
+
165
+ 示例:
166
+ @retry_on_failure(max_retries=3, delay=1, backoff=2)
167
+ def query():
168
+ ...
169
+ """
170
+ def decorator(func):
171
+ @wraps(func)
172
+ def wrapper(self, *args, **kwargs):
173
+ current_delay = delay
174
+ last_exception = None
175
+
176
+ for attempt in range(max_retries):
177
+ try:
178
+ return func(self, *args, **kwargs)
179
+ except (pymysql.OperationalError, pymysql.InterfaceError) as e:
180
+ last_exception = e
181
+ if attempt == max_retries - 1:
182
+ if hasattr(self, 'logger') and self.logger:
183
+ self.logger.error(f"重试{max_retries}次后仍然失败: {str(e)}")
184
+ raise QueryError(f"查询失败(已重试{max_retries}次): {str(e)}") from e
185
+
186
+ if hasattr(self, 'logger') and self.logger:
187
+ self.logger.warning(f"查询失败,{current_delay}秒后重试 (第{attempt + 1}/{max_retries}次): {str(e)}")
188
+
189
+ time.sleep(current_delay)
190
+ current_delay *= backoff
191
+ except Exception as e:
192
+ # 其他异常不重试
193
+ raise QueryError(f"查询失败: {str(e)}") from e
194
+
195
+ # 理论上不会到达这里
196
+ if last_exception:
197
+ raise QueryError(f"查询失败: {str(last_exception)}") from last_exception
198
+
199
+ return wrapper
200
+ return decorator
201
+
202
+
203
+ # ==================== 查询构建器 ====================
204
+
205
+
206
+ class QueryBuilder:
207
+ """
208
+ 链式查询构建器
209
+
210
+ 功能:
211
+ - 链式调用构建SQL
212
+ - 自动参数化防止注入
213
+ - 支持常见的查询操作
214
+
215
+ 示例:
216
+ builder = QueryBuilder(db)
217
+ result = (builder
218
+ .table('users')
219
+ .select('id', 'name', 'email')
220
+ .where('age > %s', 18)
221
+ .where('status = %s', 'active')
222
+ .order_by('created_at DESC')
223
+ .limit(10)
224
+ .execute())
225
+ """
226
+
227
+ def __init__(self, db: 'MYSQLQuery'):
228
+ """
229
+ 初始化查询构建器
230
+
231
+ 参数:
232
+ db: MYSQLQuery实例
233
+ """
234
+ self.db = db
235
+ self._table: Optional[str] = None
236
+ self._database: Optional[str] = None
237
+ self._fields: List[str] = ['*']
238
+ self._where_conditions: List[str] = []
239
+ self._where_params: List[Any] = []
240
+ self._order_by: Optional[str] = None
241
+ self._limit_value: Optional[int] = None
242
+ self._offset_value: Optional[int] = None
243
+
244
+ def table(self, name: str, database: Optional[str] = None) -> 'QueryBuilder':
245
+ """
246
+ 设置查询表名
247
+
248
+ 参数:
249
+ name: 表名
250
+ database: 数据库名(可选)
251
+
252
+ 返回:
253
+ self(支持链式调用)
254
+ """
255
+ self._table = validate_identifier(name, "表名")
256
+ if database:
257
+ self._database = validate_identifier(database, "数据库名")
258
+ return self
259
+
260
+ def select(self, *fields: str) -> 'QueryBuilder':
261
+ """
262
+ 设置查询字段
263
+
264
+ 参数:
265
+ fields: 字段名列表
266
+
267
+ 返回:
268
+ self(支持链式调用)
269
+ """
270
+ if fields:
271
+ self._fields = [validate_identifier(f, "字段名") for f in fields]
272
+ return self
273
+
274
+ def where(self, condition: str, *params) -> 'QueryBuilder':
275
+ """
276
+ 添加WHERE条件
277
+
278
+ 参数:
279
+ condition: 条件表达式(使用%s作为占位符)
280
+ params: 参数值
281
+
282
+ 返回:
283
+ self(支持链式调用)
284
+ """
285
+ self._where_conditions.append(condition)
286
+ self._where_params.extend(params)
287
+ return self
288
+
289
+ def order_by(self, order: str) -> 'QueryBuilder':
290
+ """
291
+ 设置排序
292
+
293
+ 参数:
294
+ order: 排序表达式(例如: "id DESC" 或 "name ASC, created_at DESC")
295
+
296
+ 返回:
297
+ self(支持链式调用)
298
+ """
299
+ self._order_by = order
300
+ return self
301
+
302
+ def limit(self, limit: int) -> 'QueryBuilder':
303
+ """
304
+ 设置返回数量限制
305
+
306
+ 参数:
307
+ limit: 限制数量
308
+
309
+ 返回:
310
+ self(支持链式调用)
311
+ """
312
+ if limit < 0:
313
+ raise ValidationError("limit必须大于等于0")
314
+ self._limit_value = limit
315
+ return self
316
+
317
+ def offset(self, offset: int) -> 'QueryBuilder':
318
+ """
319
+ 设置偏移量
320
+
321
+ 参数:
322
+ offset: 偏移量
323
+
324
+ 返回:
325
+ self(支持链式调用)
326
+ """
327
+ if offset < 0:
328
+ raise ValidationError("offset必须大于等于0")
329
+ self._offset_value = offset
330
+ return self
331
+
332
+ def build(self) -> Tuple[str, Tuple]:
333
+ """
334
+ 构建SQL语句和参数
335
+
336
+ 返回:
337
+ (sql语句, 参数元组)
338
+ """
339
+ if not self._table:
340
+ raise ValidationError("必须指定表名")
341
+
342
+ field_str = '*' if self._fields == ['*'] else ', '.join(f'`{escape_identifier(f)}`' for f in self._fields)
343
+
344
+ if self._database:
345
+ table_str = f"`{escape_identifier(self._database)}`.`{escape_identifier(self._table)}`"
346
+ elif '.' in self._table:
347
+ parts = self._table.split('.', 1)
348
+ table_str = f"`{escape_identifier(parts[0])}`.`{escape_identifier(parts[1])}`"
349
+ else:
350
+ table_str = f"`{escape_identifier(self._table)}`"
351
+
352
+ sql = f"SELECT {field_str} FROM {table_str}"
353
+
354
+ if self._where_conditions:
355
+ sql += f" WHERE {' AND '.join(self._where_conditions)}"
356
+
357
+ if self._order_by:
358
+ sql += f" ORDER BY {self._order_by}"
359
+
360
+ if self._limit_value is not None:
361
+ sql += f" LIMIT {self._limit_value}"
362
+
363
+ if self._offset_value is not None:
364
+ sql += f" OFFSET {self._offset_value}"
365
+
366
+ return sql, tuple(self._where_params)
367
+
368
+ def execute(self) -> List[Dict[str, Any]]:
369
+ """
370
+ 执行查询并返回结果
371
+
372
+ 返回:
373
+ 查询结果(字典列表)
374
+ """
375
+ sql, params = self.build()
376
+ return self.db.query_to_dict(sql, params)
377
+
378
+ def execute_one(self) -> Optional[Dict[str, Any]]:
379
+ """
380
+ 执行查询并返回第一条记录
381
+
382
+ 返回:
383
+ 查询结果(单个字典)或None
384
+ """
385
+ sql, params = self.build()
386
+ return self.db.execute_query(sql, params, fetch_one=True)
387
+
388
+
389
+ # ==================== 主管理类 ====================
390
+
391
+
392
+ class MYSQLQuery:
393
+ """
394
+ MySQL数据库管理器
395
+
396
+ 功能特性:
397
+ - 连接池管理,提高性能
398
+ - SSL安全连接
399
+ - SQL注入防护
400
+ - 灵活的日志管理
401
+ - 自动资源管理,防止内存泄漏
402
+ - 支持with上下文管理器
403
+ - 多种数据格式导出
404
+
405
+ """
406
+
407
+ # 类变量:用于跟踪所有实例
408
+ _instances = []
409
+
410
+ @classmethod
411
+ def cleanup_all(cls):
412
+ """
413
+ 清理所有活动的MYSQLQuery实例
414
+
415
+ 功能:
416
+ - 关闭所有未关闭的实例
417
+ - 清理弱引用列表
418
+
419
+ 使用场景:
420
+ - 应用程序退出前
421
+ - 测试清理
422
+ - 资源紧张时强制释放连接
423
+
424
+ 示例:
425
+ # 在应用退出时
426
+ import atexit
427
+ atexit.register(MYSQLQuery.cleanup_all)
428
+ """
429
+ count = 0
430
+ for ref in cls._instances[:]: # 使用切片副本遍历
431
+ instance = ref()
432
+ if instance and not instance._closed:
433
+ try:
434
+ instance.close()
435
+ count += 1
436
+ except:
437
+ pass # 忽略清理过程中的错误
438
+
439
+ # 清空列表
440
+ cls._instances.clear()
441
+
442
+ # 这里不记录日志,因为可能在程序关闭时调用
443
+ if count > 0:
444
+ print(f"已清理 {count} 个MySQL连接实例")
445
+
446
+ def __init__(
447
+ self,
448
+ host: str = 'localhost',
449
+ port: int = 3306,
450
+ user: str = 'root',
451
+ password: str = '',
452
+ database: str = None,
453
+ charset: str = 'utf8mb4',
454
+ pool_size: int = 2,
455
+ ssl_config: Optional[Dict[str, Any]] = None,
456
+ log_config: Optional[Dict[str, Any]] = None,
457
+ connect_timeout: int = 10,
458
+ read_timeout: int = 30,
459
+ write_timeout: int = 30
460
+ ):
461
+ """
462
+ 初始化MySQL管理器
463
+
464
+ 参数:
465
+ host: 数据库主机地址
466
+ port: 数据库端口
467
+ user: 用户名
468
+ password: 密码
469
+ database: 数据库名(可选,None表示不指定默认数据库,可随时切换)
470
+ charset: 字符集(默认utf8mb4支持emoji)
471
+ pool_size: 连接池大小
472
+ ssl_config: SSL配置字典,例如: {
473
+ 'ca': '/path/to/ca.pem',
474
+ 'cert': '/path/to/client-cert.pem',
475
+ 'key': '/path/to/client-key.pem'
476
+ }
477
+ log_config: 日志配置字典(键名和level值均忽略大小写),例如: {
478
+ 'enable': True, # 是否启用日志(默认True)
479
+ # 设置为False时,不会输出任何日志,忽略其他配置
480
+ 'level': 'INFO', # 日志级别 debug/info/warning/error(默认INFO)
481
+ 'output': 'console', # 输出位置(默认console):
482
+ # - 'console' 或 'terminal': 仅输出到终端
483
+ # - 'file': 仅输出到文件
484
+ # - 'both': 同时输出到终端和文件
485
+ 'file_path': 'mysql_query.log' # 日志文件路径(可选)
486
+ # 相对路径:存储到用户home目录
487
+ # 绝对路径:存储到指定路径
488
+ }
489
+ connect_timeout: 连接超时(秒,默认10)
490
+ read_timeout: 读取超时(秒,默认30)
491
+ write_timeout: 写入超时(秒,默认30)
492
+
493
+ 示例:
494
+ # 指定默认数据库
495
+ db = MYSQLQuery(host='localhost', user='root', password='pwd', database='db1')
496
+
497
+ # 查询多个数据库(使用 database.table 格式)
498
+ db = MYSQLQuery(host='localhost', user='root', password='pwd')
499
+ db.query_to_dict("SELECT * FROM db1.table1")
500
+ db.query_to_dict("SELECT * FROM db2.table2")
501
+
502
+ # SSL连接
503
+ db = MYSQLQuery(
504
+ host='localhost', user='root', password='pwd', database='db',
505
+ ssl_config={'ca': '/path/to/ca.pem'}
506
+ )
507
+
508
+ # 完全禁用日志(enable=False时忽略其他配置)
509
+ db = MYSQLQuery(
510
+ host='localhost', user='root', password='pwd',
511
+ log_config={'enable': False} # 不会输出任何日志
512
+ )
513
+
514
+ # 仅输出到终端(默认,适合开发调试)
515
+ db = MYSQLQuery(
516
+ host='localhost', user='root', password='pwd',
517
+ log_config={'output': 'console'} # 或 'terminal'
518
+ )
519
+
520
+ # 仅输出到文件(适合生产环境)
521
+ db = MYSQLQuery(
522
+ host='localhost', user='root', password='pwd',
523
+ log_config={
524
+ 'output': 'file',
525
+ 'file_path': '/var/log/mysql_query.log' # 绝对路径
526
+ }
527
+ )
528
+
529
+ # 同时输出到终端和文件(适合问题排查)
530
+ db = MYSQLQuery(
531
+ host='localhost', user='root', password='pwd',
532
+ log_config={
533
+ 'level': 'debug', # 'debug'/'DEBUG' 都可以
534
+ 'output': 'both', # 同时输出
535
+ 'file_path': 'mysql_query.log' # 相对路径,存到 ~/mysql_query.log
536
+ }
537
+ )
538
+
539
+ # 自定义超时
540
+ db = MYSQLQuery(
541
+ host='localhost', user='root', password='pwd',
542
+ connect_timeout=5, read_timeout=60
543
+ )
544
+
545
+ 异常:
546
+ ValidationError: 参数验证失败
547
+ ConnectionError: 连接初始化失败
548
+ """
549
+ if not host:
550
+ raise ValidationError("host 不能为空")
551
+ if not user:
552
+ raise ValidationError("user 不能为空")
553
+ if port <= 0 or port > 65535:
554
+ raise ValidationError(f"port 必须在 1-65535 之间,当前值: {port}")
555
+ if pool_size < 1:
556
+ raise ValidationError(f"pool_size 必须大于 0,当前值: {pool_size}")
557
+ if connect_timeout < 1:
558
+ raise ValidationError(f"connect_timeout 必须大于 0,当前值: {connect_timeout}")
559
+ if read_timeout < 1:
560
+ raise ValidationError(f"read_timeout 必须大于 0,当前值: {read_timeout}")
561
+ if write_timeout < 1:
562
+ raise ValidationError(f"write_timeout 必须大于 0,当前值: {write_timeout}")
563
+
564
+ if database:
565
+ database = validate_identifier(database, "数据库名")
566
+
567
+ self.host = host
568
+ self.port = port
569
+ self.user = user
570
+ self.password = password
571
+ self.database = database
572
+ self.charset = charset
573
+ self.ssl_config = ssl_config
574
+ self.connect_timeout = connect_timeout
575
+ self.read_timeout = read_timeout
576
+ self.write_timeout = write_timeout
577
+ self._closed = False
578
+
579
+ self._setup_logger(log_config or {})
580
+
581
+ try:
582
+ self._init_pool(pool_size)
583
+ except Exception as e:
584
+ raise ConnectionError(f"连接池初始化失败: {str(e)}") from e
585
+
586
+ MYSQLQuery._instances.append(weakref.ref(self))
587
+
588
+ if self.logger:
589
+ masked_pwd = mask_password(password)
590
+ self.logger.debug(f"MySQL管理器初始化: {user}@{host}:{port}, 密码: {masked_pwd}")
591
+ if database:
592
+ self.logger.info(f"默认数据库: {database}")
593
+ if ssl_config:
594
+ self.logger.info("SSL连接已启用")
595
+ self.logger.debug(f"超时配置 - 连接:{connect_timeout}s, 读:{read_timeout}s, 写:{write_timeout}s")
596
+
597
+ def _setup_logger(self, log_config: Dict[str, Any]):
598
+ """
599
+ 配置日志系统
600
+
601
+ 参数:
602
+ log_config: 日志配置字典
603
+ """
604
+ config = {k.lower(): v for k, v in log_config.items()}
605
+
606
+ enable = config.get('enable', True)
607
+
608
+ if not enable:
609
+ self.logger = logging.getLogger(f'MYSQLQuery_{id(self)}')
610
+ self.logger.addHandler(logging.NullHandler())
611
+ self.logger.propagate = False
612
+ return
613
+
614
+ level = str(config.get('level', 'INFO')).upper()
615
+ output = str(config.get('output', 'console')).lower()
616
+ file_path = config.get('file_path')
617
+
618
+ self.logger = logging.getLogger(f'MYSQLQuery_{id(self)}')
619
+ self.logger.setLevel(getattr(logging, level))
620
+ self.logger.propagate = False
621
+ self.logger.handlers.clear()
622
+
623
+ formatter = logging.Formatter(
624
+ '%(asctime)s - %(levelname)s - %(message)s',
625
+ datefmt='%Y-%m-%d %H:%M:%S'
626
+ )
627
+
628
+ # 输出到终端
629
+ if output in ('console', 'terminal', 'both'):
630
+ console_handler = logging.StreamHandler()
631
+ console_handler.setLevel(logging.DEBUG)
632
+ console_handler.setFormatter(formatter)
633
+ self.logger.addHandler(console_handler)
634
+
635
+ # 输出到文件
636
+ if output in ('file', 'both'):
637
+ if not file_path:
638
+ file_path = Path.home() / 'mysql_query.log'
639
+ else:
640
+ file_path = Path(file_path)
641
+ if not file_path.is_absolute():
642
+ file_path = Path.home() / file_path
643
+
644
+ file_path.parent.mkdir(parents=True, exist_ok=True)
645
+
646
+ file_handler = logging.FileHandler(file_path, encoding='utf-8')
647
+ file_handler.setLevel(logging.DEBUG)
648
+ file_handler.setFormatter(formatter)
649
+ self.logger.addHandler(file_handler)
650
+
651
+ if output == 'both':
652
+ self.logger.debug(f"日志文件: {file_path}")
653
+
654
+ def _init_pool(self, pool_size: int):
655
+ """
656
+ 初始化数据库连接池
657
+
658
+ 参数:
659
+ pool_size: 连接池大小
660
+ """
661
+ try:
662
+ connection_kwargs = {
663
+ 'host': self.host,
664
+ 'port': self.port,
665
+ 'user': self.user,
666
+ 'password': self.password,
667
+ 'charset': self.charset,
668
+ 'cursorclass': DictCursor,
669
+ 'autocommit': True,
670
+ 'connect_timeout': self.connect_timeout,
671
+ 'read_timeout': self.read_timeout,
672
+ 'write_timeout': self.write_timeout,
673
+ }
674
+
675
+ if self.database:
676
+ connection_kwargs['database'] = self.database
677
+
678
+ if self.ssl_config:
679
+ ssl_dict = {}
680
+ if 'ca' in self.ssl_config:
681
+ ssl_dict['ca'] = self.ssl_config['ca']
682
+ if 'cert' in self.ssl_config:
683
+ ssl_dict['cert'] = self.ssl_config['cert']
684
+ if 'key' in self.ssl_config:
685
+ ssl_dict['key'] = self.ssl_config['key']
686
+ if 'check_hostname' in self.ssl_config:
687
+ ssl_dict['check_hostname'] = self.ssl_config['check_hostname']
688
+
689
+ connection_kwargs['ssl'] = ssl_dict
690
+ if self.logger:
691
+ self.logger.debug(f"SSL配置已应用: {list(ssl_dict.keys())}")
692
+
693
+ # 创建连接池
694
+ self.pool = PooledDB(
695
+ creator=pymysql,
696
+ maxconnections=pool_size,
697
+ mincached=1,
698
+ maxcached=3,
699
+ blocking=True,
700
+ maxusage=10000,
701
+ ping=1,
702
+ **connection_kwargs
703
+ )
704
+
705
+ if self.logger:
706
+ self.logger.debug(f"连接池初始化成功,大小: {pool_size}")
707
+ except Exception as e:
708
+ if self.logger:
709
+ self.logger.error(f"连接池初始化失败: {str(e)}")
710
+ raise
711
+
712
+ @contextmanager
713
+ def _get_connection(self):
714
+ """
715
+ 获取数据库连接(上下文管理器)
716
+ 自动管理连接的获取和释放
717
+ """
718
+ conn = None
719
+ try:
720
+ conn = self.pool.connection()
721
+ yield conn
722
+ except Exception as e:
723
+ if self.logger:
724
+ self.logger.error(f"数据库连接错误: {str(e)}")
725
+ raise ConnectionError(f"数据库连接错误: {str(e)}") from e
726
+ finally:
727
+ if conn:
728
+ conn.close()
729
+
730
+ @contextmanager
731
+ def transaction(self):
732
+ """
733
+ 事务上下文管理器
734
+
735
+ 功能:
736
+ - 自动开始事务
737
+ - 执行成功时自动提交
738
+ - 发生异常时自动回滚
739
+ - 自动管理连接资源
740
+
741
+ 示例:
742
+ with db.transaction() as cursor:
743
+ cursor.execute("INSERT INTO users (name) VALUES (%s)", ('张三',))
744
+ cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1")
745
+ cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE user_id = 2")
746
+
747
+ 异常:
748
+ TransactionError: 事务处理失败
749
+ """
750
+ conn = None
751
+ cursor = None
752
+ try:
753
+ conn = self.pool.connection()
754
+ conn.begin()
755
+ cursor = conn.cursor()
756
+
757
+ if self.logger:
758
+ self.logger.debug("事务已开始")
759
+
760
+ yield cursor
761
+
762
+ conn.commit()
763
+ if self.logger:
764
+ self.logger.debug("事务已提交")
765
+
766
+ except Exception as e:
767
+ if conn:
768
+ try:
769
+ conn.rollback()
770
+ if self.logger:
771
+ self.logger.warning(f"事务已回滚: {str(e)}")
772
+ except Exception as rollback_error:
773
+ if self.logger:
774
+ self.logger.error(f"事务回滚失败: {str(rollback_error)}")
775
+ raise TransactionError(f"事务执行失败: {str(e)}") from e
776
+ finally:
777
+ if cursor:
778
+ cursor.close()
779
+ if conn:
780
+ conn.close()
781
+
782
+ def execute_many(
783
+ self,
784
+ sql: str,
785
+ params_list: List[Union[Tuple, Dict]],
786
+ batch_size: int = 1000
787
+ ) -> int:
788
+ """
789
+ 批量执行SQL(在事务中执行)
790
+
791
+ 参数:
792
+ sql: SQL语句(INSERT, UPDATE, DELETE等)
793
+ params_list: 参数列表
794
+ batch_size: 批次大小(默认1000,避免一次性提交过多数据)
795
+
796
+ 返回:
797
+ 影响的总行数
798
+
799
+ 示例:
800
+ # 批量插入
801
+ sql = "INSERT INTO users (name, age) VALUES (%s, %s)"
802
+ params = [('张三', 20), ('李四', 25), ('王五', 30)]
803
+ affected = db.execute_many(sql, params)
804
+
805
+ 异常:
806
+ TransactionError: 批量执行失败
807
+ """
808
+ if not params_list:
809
+ if self.logger:
810
+ self.logger.warning("参数列表为空,跳过执行")
811
+ return 0
812
+
813
+ total_affected = 0
814
+ start_time = datetime.now()
815
+
816
+ try:
817
+ for i in range(0, len(params_list), batch_size):
818
+ batch = params_list[i:i + batch_size]
819
+
820
+ with self.transaction() as cursor:
821
+ cursor.executemany(sql, batch)
822
+ total_affected += cursor.rowcount
823
+
824
+ if self.logger:
825
+ self.logger.debug(f"批次 {i//batch_size + 1}: 影响 {cursor.rowcount} 行")
826
+
827
+ elapsed = (datetime.now() - start_time).total_seconds()
828
+ if self.logger:
829
+ self.logger.info(f"批量执行成功: 总计 {total_affected} 行, 耗时 {elapsed:.3f}秒")
830
+
831
+ return total_affected
832
+
833
+ except Exception as e:
834
+ if self.logger:
835
+ self.logger.error(f"批量执行失败: {str(e)}")
836
+ raise TransactionError(f"批量执行失败: {str(e)}") from e
837
+
838
+ def builder(self) -> QueryBuilder:
839
+ """
840
+ 获取查询构建器实例
841
+
842
+ 返回:
843
+ QueryBuilder对象
844
+
845
+ 示例:
846
+ result = db.builder() \\
847
+ .table('users') \\
848
+ .select('id', 'name') \\
849
+ .where('age > %s', 18) \\
850
+ .limit(10) \\
851
+ .execute()
852
+ """
853
+ return QueryBuilder(self)
854
+
855
+ def __enter__(self):
856
+ """上下文管理器入口"""
857
+ return self
858
+
859
+ def __exit__(self, exc_type, exc_val, exc_tb):
860
+ """
861
+ 上下文管理器出口,自动关闭连接
862
+
863
+ 参数:
864
+ exc_type: 异常类型
865
+ exc_val: 异常值
866
+ exc_tb: 异常追踪
867
+ """
868
+ self.close()
869
+ return False # 不抑制异常
870
+
871
+ def __del__(self):
872
+ """
873
+ 析构函数,确保资源被释放
874
+ 防止内存泄漏
875
+ """
876
+ if not self._closed and hasattr(self, 'pool'):
877
+ try:
878
+ self.close()
879
+ except:
880
+ # 在析构函数中忽略所有异常
881
+ pass
882
+
883
+ @retry_on_failure(max_retries=3, delay=1.0, backoff=2.0)
884
+ def execute_query(
885
+ self,
886
+ sql: str,
887
+ params: Optional[Union[Tuple, Dict]] = None,
888
+ fetch_one: bool = False
889
+ ) -> Union[List[Dict], Dict, None]:
890
+ """
891
+ 执行SQL查询
892
+
893
+ 参数:
894
+ sql: SQL查询语句(支持参数化查询,使用%s占位符)
895
+ params: 查询参数(元组或字典)
896
+ fetch_one: 是否只返回一条记录
897
+
898
+ 返回:
899
+ 查询结果(字典列表或单个字典)
900
+
901
+ 异常:
902
+ QueryError: 查询执行失败
903
+
904
+ 注意:
905
+ 网络错误会自动重试最多3次
906
+ """
907
+ start_time = datetime.now()
908
+
909
+ try:
910
+ with self._get_connection() as conn:
911
+ with conn.cursor() as cursor:
912
+ cursor.execute(sql, params)
913
+
914
+ if fetch_one:
915
+ result = cursor.fetchone()
916
+ else:
917
+ result = cursor.fetchall()
918
+
919
+ elapsed = (datetime.now() - start_time).total_seconds()
920
+ if self.logger:
921
+ row_count = len(result) if result and not fetch_one else 1 if result else 0
922
+ self.logger.debug(f"查询成功: {row_count}行, 耗时{elapsed:.3f}秒")
923
+
924
+ return result
925
+
926
+ except Exception as e:
927
+ if self.logger:
928
+ self.logger.error(f"查询失败: {str(e)}")
929
+ self.logger.debug(f"SQL: {sql}")
930
+ raise
931
+
932
+ def query_to_dict(
933
+ self,
934
+ sql: str,
935
+ params: Optional[Union[Tuple, Dict]] = None
936
+ ) -> List[Dict[str, Any]]:
937
+ """
938
+ 查询并返回字典列表格式
939
+
940
+ 参数:
941
+ sql: SQL查询语句
942
+ params: 查询参数
943
+
944
+ 返回:
945
+ 字典列表 [{'field1': value1, 'field2': value2}, ...]
946
+ """
947
+ return self.execute_query(sql, params, fetch_one=False) or []
948
+
949
+ def query_to_dataframe(
950
+ self,
951
+ sql: str,
952
+ params: Optional[Union[Tuple, Dict]] = None
953
+ ):
954
+ """
955
+ 查询并返回DataFrame格式
956
+
957
+ 参数:
958
+ sql: SQL查询语句
959
+ params: 查询参数
960
+
961
+ 返回:
962
+ pandas.DataFrame对象
963
+ """
964
+ try:
965
+ import pandas as pd
966
+ except ImportError:
967
+ if self.logger:
968
+ self.logger.error("需要安装pandas库: pip install pandas")
969
+ raise ImportError("请先安装pandas: pip install pandas")
970
+
971
+ result = self.execute_query(sql, params, fetch_one=False)
972
+ df = pd.DataFrame(result if result else [])
973
+
974
+ if self.logger:
975
+ self.logger.debug(f"DataFrame: {df.shape}")
976
+ return df
977
+
978
+ def query_with_fields(
979
+ self,
980
+ table: str,
981
+ fields: Optional[List[str]] = None,
982
+ where: Optional[str] = None,
983
+ params: Optional[Union[Tuple, Dict]] = None,
984
+ order_by: Optional[str] = None,
985
+ limit: Optional[int] = None,
986
+ database: str = None
987
+ ) -> List[Dict[str, Any]]:
988
+ """
989
+ 查询指定字段
990
+
991
+ 参数:
992
+ table: 表名(支持 database.table 格式)
993
+ fields: 字段列表(None表示查询所有字段)
994
+ where: WHERE条件(不含WHERE关键字)
995
+ params: WHERE条件参数
996
+ order_by: 排序字段(例如: "id DESC" 或 "name ASC, created_at DESC")
997
+ limit: 限制返回数量
998
+ database: 数据库名(None表示使用当前数据库或table中指定的数据库)
999
+
1000
+ 返回:
1001
+ 字典列表
1002
+
1003
+ 示例:
1004
+ data = db.query_with_fields(
1005
+ table='users',
1006
+ fields=["id", "name", "email"],
1007
+ where="age > %s",
1008
+ params=(18,),
1009
+ order_by="created_at DESC",
1010
+ limit=10
1011
+ )
1012
+ """
1013
+ field_str = '*' if not fields else ', '.join(f'`{f}`' for f in fields)
1014
+
1015
+ if database:
1016
+ table_str = f"`{database}`.`{table}`"
1017
+ elif '.' in table:
1018
+ parts = table.split('.', 1)
1019
+ table_str = f"`{parts[0]}`.`{parts[1]}`"
1020
+ else:
1021
+ table_str = f"`{table}`"
1022
+
1023
+ sql = f"SELECT {field_str} FROM {table_str}"
1024
+
1025
+ if where:
1026
+ sql += f" WHERE {where}"
1027
+
1028
+ if order_by:
1029
+ sql += f" ORDER BY {order_by}"
1030
+
1031
+ if limit:
1032
+ sql += f" LIMIT {limit}"
1033
+
1034
+ return self.query_to_dict(sql, params)
1035
+
1036
+ def get_table_structure(self, table: str) -> List[FieldInfo]:
1037
+ """
1038
+ 获取表结构信息
1039
+
1040
+ 参数:
1041
+ table: 表名
1042
+
1043
+ 返回:
1044
+ 表结构信息列表,包含字段名、类型、是否为空、键信息等
1045
+
1046
+ 异常:
1047
+ ValidationError: 表名验证失败
1048
+ QueryError: 查询失败
1049
+ """
1050
+ table = validate_identifier(table, "表名")
1051
+ sql = f"DESCRIBE `{escape_identifier(table)}`"
1052
+ result = self.execute_query(sql)
1053
+
1054
+ if self.logger:
1055
+ count = len(result) if result else 0
1056
+ self.logger.debug(f"表结构: {table}, {count}个字段")
1057
+ return result or []
1058
+
1059
+ def get_table_info(self, table: str, database: Optional[str] = None) -> Optional[TableInfo]:
1060
+ """
1061
+ 获取表的详细信息
1062
+
1063
+ 参数:
1064
+ table: 表名
1065
+ database: 数据库名(None表示使用当前数据库)
1066
+
1067
+ 返回:
1068
+ 表信息字典(包含引擎、字符集、注释等),如果表不存在则返回None
1069
+
1070
+ 异常:
1071
+ ValidationError: 表名或数据库名验证失败
1072
+ QueryError: 查询失败
1073
+ """
1074
+ table = validate_identifier(table, "表名")
1075
+
1076
+ target_db = database if database else self.database
1077
+ if not target_db:
1078
+ raise ValidationError("必须指定数据库名或在初始化时设置默认数据库")
1079
+
1080
+ target_db = validate_identifier(target_db, "数据库名")
1081
+
1082
+ sql = """
1083
+ SELECT
1084
+ TABLE_NAME as table_name,
1085
+ ENGINE as engine,
1086
+ TABLE_ROWS as row_count,
1087
+ AVG_ROW_LENGTH as avg_row_length,
1088
+ DATA_LENGTH as data_length,
1089
+ INDEX_LENGTH as index_length,
1090
+ AUTO_INCREMENT as auto_increment,
1091
+ CREATE_TIME as create_time,
1092
+ UPDATE_TIME as update_time,
1093
+ TABLE_COLLATION as collation,
1094
+ TABLE_COMMENT as comment
1095
+ FROM information_schema.TABLES
1096
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
1097
+ """
1098
+ result = self.execute_query(sql, (target_db, table), fetch_one=True)
1099
+
1100
+ if self.logger and result:
1101
+ self.logger.debug(f"表信息: {target_db}.{table}")
1102
+
1103
+ return result
1104
+
1105
+ def list_tables(self, database: Optional[str] = None) -> List[str]:
1106
+ """
1107
+ 获取数据库的所有表名
1108
+
1109
+ 参数:
1110
+ database: 数据库名(None表示使用当前数据库)
1111
+
1112
+ 返回:
1113
+ 表名列表
1114
+
1115
+ 异常:
1116
+ ValidationError: 数据库名验证失败
1117
+ QueryError: 查询失败
1118
+ """
1119
+ if database:
1120
+ database = validate_identifier(database, "数据库名")
1121
+ sql = f"SHOW TABLES FROM `{escape_identifier(database)}`"
1122
+ else:
1123
+ sql = "SHOW TABLES"
1124
+
1125
+ result = self.execute_query(sql)
1126
+ tables = [list(row.values())[0] for row in result] if result else []
1127
+
1128
+ if self.logger:
1129
+ db_name = database if database else "当前数据库"
1130
+ self.logger.debug(f"{db_name}共{len(tables)}张表")
1131
+ return tables
1132
+
1133
+ def get_current_database(self) -> Optional[str]:
1134
+ """
1135
+ 获取当前使用的数据库名
1136
+
1137
+ 返回:
1138
+ 当前数据库名,如果未选择数据库则返回None
1139
+ """
1140
+ sql = "SELECT DATABASE() as db"
1141
+ result = self.execute_query(sql, fetch_one=True)
1142
+ return result.get('db') if result else None
1143
+
1144
+ def list_databases(self) -> List[str]:
1145
+ """
1146
+ 获取所有数据库名称列表
1147
+
1148
+ 返回:
1149
+ 数据库名称列表
1150
+ """
1151
+ sql = "SHOW DATABASES"
1152
+ result = self.execute_query(sql)
1153
+ databases = [list(row.values())[0] for row in result] if result else []
1154
+
1155
+ if self.logger:
1156
+ self.logger.debug(f"共{len(databases)}个数据库")
1157
+ return databases
1158
+
1159
+ def export_to_json(
1160
+ self,
1161
+ sql: str,
1162
+ params: Optional[Union[Tuple, Dict]] = None,
1163
+ file_path: Optional[str] = None,
1164
+ indent: int = 2,
1165
+ fields: Optional[List[str]] = None
1166
+ ) -> str:
1167
+ """
1168
+ 查询并导出为JSON文件
1169
+
1170
+ 参数:
1171
+ sql: SQL查询语句
1172
+ params: 查询参数
1173
+ file_path: 导出文件路径(None则自动生成到home目录)
1174
+ indent: JSON缩进空格数
1175
+ fields: 要导出的字段列表(None表示导出所有字段)
1176
+
1177
+ 返回:
1178
+ 导出文件的完整路径
1179
+ """
1180
+ # 查询数据
1181
+ data = self.query_to_dict(sql, params)
1182
+
1183
+ # 过滤字段
1184
+ if fields and data:
1185
+ data = [{k: v for k, v in row.items() if k in fields} for row in data]
1186
+
1187
+ # 确定导出路径
1188
+ if not file_path:
1189
+ home_dir = Path.home()
1190
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1191
+ file_path = home_dir / f"mysql_export_{timestamp}.json"
1192
+ else:
1193
+ file_path = Path(file_path)
1194
+
1195
+ # 创建目录
1196
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1197
+
1198
+ # 写入文件
1199
+ with open(file_path, 'w', encoding='utf-8') as f:
1200
+ json.dump(data, f, ensure_ascii=False, indent=indent, default=str)
1201
+
1202
+ if self.logger:
1203
+ self.logger.info(f"导出JSON: {file_path}, {len(data)}行")
1204
+ return str(file_path)
1205
+
1206
+ def export_to_csv(
1207
+ self,
1208
+ sql: str,
1209
+ params: Optional[Union[Tuple, Dict]] = None,
1210
+ file_path: Optional[str] = None,
1211
+ encoding: str = 'utf-8-sig',
1212
+ fields: Optional[List[str]] = None
1213
+ ) -> str:
1214
+ """
1215
+ 查询并导出为CSV文件
1216
+
1217
+ 参数:
1218
+ sql: SQL查询语句
1219
+ params: 查询参数
1220
+ file_path: 导出文件路径(None则自动生成到home目录)
1221
+ encoding: 文件编码(utf-8-sig支持Excel直接打开)
1222
+ fields: 要导出的字段列表(None表示导出所有字段)
1223
+
1224
+ 返回:
1225
+ 导出文件的完整路径
1226
+ """
1227
+ try:
1228
+ import pandas as pd
1229
+ except ImportError:
1230
+ if self.logger:
1231
+ self.logger.error("需要安装pandas库: pip install pandas")
1232
+ raise ImportError("请先安装pandas: pip install pandas")
1233
+
1234
+ # 查询数据
1235
+ df = self.query_to_dataframe(sql, params)
1236
+
1237
+ # 过滤字段
1238
+ if fields and not df.empty:
1239
+ df = df[[col for col in fields if col in df.columns]]
1240
+
1241
+ # 确定导出路径
1242
+ if not file_path:
1243
+ home_dir = Path.home()
1244
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1245
+ file_path = home_dir / f"mysql_export_{timestamp}.csv"
1246
+ else:
1247
+ file_path = Path(file_path)
1248
+
1249
+ # 创建目录
1250
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1251
+
1252
+ # 写入CSV
1253
+ df.to_csv(file_path, index=False, encoding=encoding)
1254
+
1255
+ if self.logger:
1256
+ self.logger.info(f"导出CSV: {file_path}, {len(df)}行")
1257
+ return str(file_path)
1258
+
1259
+ def export_to_excel(
1260
+ self,
1261
+ sql: str,
1262
+ params: Optional[Union[Tuple, Dict]] = None,
1263
+ file_path: Optional[str] = None,
1264
+ sheet_name: str = 'Sheet1',
1265
+ fields: Optional[List[str]] = None
1266
+ ) -> str:
1267
+ """
1268
+ 查询并导出为Excel文件
1269
+
1270
+ 参数:
1271
+ sql: SQL查询语句
1272
+ params: 查询参数
1273
+ file_path: 导出文件路径(None则自动生成到home目录)
1274
+ sheet_name: 工作表名称
1275
+ fields: 要导出的字段列表(None表示导出所有字段)
1276
+
1277
+ 返回:
1278
+ 导出文件的完整路径
1279
+ """
1280
+ try:
1281
+ import pandas as pd
1282
+ except ImportError:
1283
+ if self.logger:
1284
+ self.logger.error("需要安装pandas和openpyxl库")
1285
+ raise ImportError("请先安装: pip install pandas openpyxl")
1286
+
1287
+ # 查询数据
1288
+ df = self.query_to_dataframe(sql, params)
1289
+
1290
+ # 过滤字段
1291
+ if fields and not df.empty:
1292
+ df = df[[col for col in fields if col in df.columns]]
1293
+
1294
+ # 确定导出路径
1295
+ if not file_path:
1296
+ home_dir = Path.home()
1297
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1298
+ file_path = home_dir / f"mysql_export_{timestamp}.xlsx"
1299
+ else:
1300
+ file_path = Path(file_path)
1301
+
1302
+ # 创建目录
1303
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1304
+
1305
+ # 写入Excel
1306
+ with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
1307
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
1308
+
1309
+ if self.logger:
1310
+ self.logger.info(f"导出Excel: {file_path}, {len(df)}行")
1311
+ return str(file_path)
1312
+
1313
+ def export_table_structure(
1314
+ self,
1315
+ table: str,
1316
+ file_path: Optional[str] = None,
1317
+ format: str = 'json'
1318
+ ) -> str:
1319
+ """
1320
+ 导出表结构到文件
1321
+
1322
+ 参数:
1323
+ table: 表名
1324
+ file_path: 导出文件路径(None则自动生成到home目录)
1325
+ format: 导出格式 (json/csv/excel)
1326
+
1327
+ 返回:
1328
+ 导出文件的完整路径
1329
+ """
1330
+ # 获取表结构
1331
+ structure = self.get_table_structure(table)
1332
+
1333
+ # 获取表信息
1334
+ table_info = self.get_table_info(table)
1335
+
1336
+ # 组合数据
1337
+ export_data = {
1338
+ 'table_info': table_info,
1339
+ 'structure': structure
1340
+ }
1341
+
1342
+ # 确定导出路径
1343
+ if not file_path:
1344
+ home_dir = Path.home()
1345
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1346
+ file_path = home_dir / f"table_structure_{table}_{timestamp}.{format}"
1347
+ else:
1348
+ file_path = Path(file_path)
1349
+
1350
+ # 创建目录
1351
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1352
+
1353
+ # 根据格式导出
1354
+ if format == 'json':
1355
+ with open(file_path, 'w', encoding='utf-8') as f:
1356
+ json.dump(export_data, f, ensure_ascii=False, indent=2, default=str)
1357
+ elif format in ['csv', 'excel']:
1358
+ try:
1359
+ import pandas as pd
1360
+ df_structure = pd.DataFrame(structure)
1361
+
1362
+ if format == 'csv':
1363
+ df_structure.to_csv(file_path, index=False, encoding='utf-8-sig')
1364
+ else: # excel
1365
+ with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
1366
+ df_structure.to_excel(writer, sheet_name='Structure', index=False)
1367
+ if table_info:
1368
+ df_info = pd.DataFrame([table_info])
1369
+ df_info.to_excel(writer, sheet_name='Info', index=False)
1370
+ except ImportError:
1371
+ if self.logger:
1372
+ self.logger.error("需要安装pandas库")
1373
+ raise
1374
+ else:
1375
+ raise ValueError(f"不支持的格式: {format}")
1376
+
1377
+ if self.logger:
1378
+ self.logger.debug(f"表结构已导出: {file_path}")
1379
+ return str(file_path)
1380
+
1381
+ def test_connection(self) -> bool:
1382
+ """
1383
+ 测试数据库连接
1384
+
1385
+ 返回:
1386
+ 连接是否成功
1387
+ """
1388
+ try:
1389
+ sql = "SELECT VERSION() as version"
1390
+ result = self.execute_query(sql, fetch_one=True)
1391
+ if result:
1392
+ version = result.get('version', 'Unknown')
1393
+ if self.logger:
1394
+ self.logger.info(f"连接成功, MySQL {version}")
1395
+ return True
1396
+ return False
1397
+ except Exception as e:
1398
+ if self.logger:
1399
+ self.logger.error(f"连接失败: {str(e)}")
1400
+ return False
1401
+
1402
+ def close(self):
1403
+ """
1404
+ 关闭连接池
1405
+
1406
+ 注意:
1407
+ - 可以重复调用,不会报错
1408
+ - 关闭后不能再执行查询
1409
+ - 使用with语句会自动调用此方法
1410
+ """
1411
+ if self._closed:
1412
+ return
1413
+
1414
+ try:
1415
+ if hasattr(self, 'pool'):
1416
+ self.pool.close()
1417
+ if self.logger:
1418
+ self.logger.debug("连接池已关闭")
1419
+ except Exception as e:
1420
+ if self.logger:
1421
+ self.logger.error(f"关闭连接失败: {str(e)}")
1422
+ finally:
1423
+ self._closed = True
1424
+
1425
+
1426
+ def test():
1427
+ try:
1428
+ with MYSQLQuery(
1429
+ host='localhost',
1430
+ port=3306,
1431
+ user='user',
1432
+ password='password',
1433
+ log_config={'enable': True, 'level': 'INFO', 'output': 'both', 'file_path': 'mysql_query.log'}
1434
+ ) as db:
1435
+ data = db.query_with_fields(
1436
+ table='推广数据_圣积天猫店.营销场景报表_2025',
1437
+ fields=['日期', '店铺名称', '花费', '点击量', '展现量', '总成交笔数', '总成交金额'],
1438
+ where='花费 > 0 and 日期 >= "2025-09-27"',
1439
+ )
1440
+ for row in data:
1441
+ print(row)
1442
+
1443
+ except ValidationError as e:
1444
+ print(f"✗ 参数验证失败: {e}")
1445
+ except ConnectionError as e:
1446
+ print(f"✗ 连接失败: {e}")
1447
+ except Exception as e:
1448
+ print(f"✗ 测试失败: {e}")
1449
+
1450
+ if __name__ == '__main__':
1451
+ # 运行测试
1452
+ test()