springbootAI 1.8.0__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.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1382 @@
1
+ """
2
+ PyMyBatis SqlSession模块
3
+
4
+ SqlSession是PyMyBatis的核心执行引擎,负责执行SQL语句、管理事务和连接
5
+ """
6
+
7
+ import logging
8
+ import contextlib
9
+ import os
10
+ import re
11
+ import dataclasses
12
+ import importlib
13
+ import copy
14
+ from collections.abc import Mapping
15
+ from typing import Any, Dict, List, Optional, Type, Tuple
16
+
17
+ from ..configuration import Configuration
18
+ from ..dialect import Dialect, get_dialect
19
+ from ..pool import ConnectionPool, create_connection_pool
20
+ from ..xml_parser import XmlParser, MappedStatement, ResultMap
21
+ from ..dynamic_sql import DynamicSQLProcessor, SecurityError
22
+ from ..cache import SqlCache, ResultMapCache, GLOBAL_PRECOMPILED_CACHE
23
+ from ..mapper import MapperProxy, MapperRegistry
24
+ from ..transaction import TransactionManager, TransactionIsolationLevel
25
+ from ..security import SQLInjectionDetector, SensitiveDataMasker, PasswordEncoder
26
+ from ..security.access_control import RoleBasedAccessControl
27
+ from ..type_handler import DEFAULT_REGISTRY, TypeHandlerRegistry
28
+ from ..interceptor import InterceptorChain
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def create_pool_from_configuration(configuration: Configuration) -> ConnectionPool:
34
+ """Create one connection pool from a validated ORM configuration."""
35
+ datasource = configuration.get_datasource()
36
+ pool_config = {
37
+ **datasource,
38
+ 'min_size': configuration.pool_min_size,
39
+ 'max_size': configuration.pool_max_size,
40
+ 'max_idle': configuration.pool_max_idle,
41
+ 'wait_timeout': configuration.pool_wait_timeout,
42
+ 'validation_interval': configuration.pool_validation_interval,
43
+ 'leak_detection_enabled': configuration.leak_detection_enabled,
44
+ 'leak_timeout': configuration.leak_timeout,
45
+ 'circuit_breaker_enabled': configuration.circuit_breaker_enabled,
46
+ 'circuit_breaker_failure_threshold': configuration.circuit_breaker_failure_threshold,
47
+ 'circuit_breaker_recovery_timeout': configuration.circuit_breaker_recovery_timeout,
48
+ 'circuit_breaker_success_threshold': configuration.circuit_breaker_success_threshold,
49
+ }
50
+ return create_connection_pool(configuration.dialect, pool_config)
51
+
52
+
53
+ class SqlSession:
54
+ """
55
+ SqlSession是PyMyBatis的核心执行引擎
56
+
57
+ 核心功能:
58
+ 1. 执行SQL语句(SELECT/INSERT/UPDATE/DELETE)
59
+ 2. 获取Mapper代理对象
60
+ 3. 管理事务
61
+ 4. 管理数据库连接
62
+ 5. 缓存管理
63
+ 6. 安全防护(SQL注入、DDL阻止、访问控制)
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ configuration: Configuration,
69
+ connection_pool: Optional[ConnectionPool] = None,
70
+ ):
71
+ """
72
+ 初始化SqlSession
73
+
74
+ Args:
75
+ configuration: 配置对象
76
+ """
77
+ self.configuration = configuration
78
+
79
+ # 获取方言
80
+ self.dialect: Dialect = get_dialect(configuration.dialect)
81
+
82
+ # Factory创建的Session共享连接池;直接创建Session时由Session拥有连接池。
83
+ self._owns_connection_pool = connection_pool is None
84
+ self.connection_pool = connection_pool or create_pool_from_configuration(configuration)
85
+
86
+ # 初始化XML解析器和映射语句缓存
87
+ self.mapped_statements: Dict[str, MappedStatement] = {}
88
+ self.result_maps: Dict[str, ResultMap] = {}
89
+ self._mapper_locations_loaded = False
90
+ self.type_handler_registry: TypeHandlerRegistry = TypeHandlerRegistry()
91
+ for java_type, handler in configuration.type_handlers.items():
92
+ if isinstance(java_type, type):
93
+ self.type_handler_registry.register(java_type, handler)
94
+
95
+ # 动态SQL处理器(传递配置)
96
+ # 根据方言选择参数占位符:SQLite/Oracle使用'?',MySQL/PostgreSQL使用'%s'
97
+ placeholder = '?' if configuration.dialect.lower() in ['sqlite', 'oracle'] else '%s'
98
+ self.dynamic_sql_processor = DynamicSQLProcessor(placeholder=placeholder)
99
+ self.dynamic_sql_processor.enable_raw_params(configuration.allow_raw_params)
100
+
101
+ # 缓存
102
+ self.sql_cache = SqlCache(
103
+ cache_type=configuration.cache_type,
104
+ max_size=configuration.cache_size,
105
+ ttl=configuration.cache_ttl
106
+ )
107
+ self.result_map_cache = ResultMapCache()
108
+
109
+ # Mapper注册中心
110
+ self.mapper_registry = MapperRegistry()
111
+
112
+ # 事务管理器
113
+ self.transaction_manager = TransactionManager(
114
+ isolation_level=TransactionIsolationLevel(configuration.default_transaction_isolation)
115
+ )
116
+
117
+ # 安全组件
118
+ self.sql_injection_detector = SQLInjectionDetector(
119
+ enabled=configuration.sql_injection_detection,
120
+ block_ddl=configuration.block_ddl,
121
+ allow_raw_params=configuration.allow_raw_params
122
+ )
123
+ self.sensitive_data_masker = SensitiveDataMasker(
124
+ enabled=configuration.sensitive_data_masking
125
+ )
126
+ self.password_encoder = PasswordEncoder()
127
+
128
+ # 访问控制
129
+ self.access_control = RoleBasedAccessControl(
130
+ enabled=configuration.access_control_enabled
131
+ )
132
+
133
+ # 当前连接
134
+ self._current_connection = None
135
+ self._current_pooled_conn = None
136
+
137
+ # 当前用户上下文(用于访问控制)
138
+ self._user_context: Dict[str, Any] = {}
139
+
140
+ self._transaction_depth = 0
141
+ self._transaction_rollback_only = False
142
+ self._transaction_dirty = False
143
+ self._transaction_flush_cache = False
144
+ self._transaction_isolation: Optional[TransactionIsolationLevel] = None
145
+ self._closed = False
146
+ self._batch_operations: List[Tuple[str, Optional[Dict[str, Any]]]] = []
147
+ self.interceptor_chain = InterceptorChain()
148
+ for interceptor in configuration.interceptors:
149
+ self.interceptor_chain.add_interceptor(interceptor)
150
+
151
+ def set_user_context(self, user_context: Dict[str, Any]) -> None:
152
+ """
153
+ 设置用户上下文(用于访问控制)
154
+
155
+ Args:
156
+ user_context: 用户上下文(包含user_id、role等)
157
+ """
158
+ self._user_context = user_context
159
+
160
+ def get_user_context(self) -> Dict[str, Any]:
161
+ """获取当前用户上下文"""
162
+ return self._user_context
163
+
164
+ def _load_mapper_locations(self) -> None:
165
+ """加载XML映射文件(支持懒加载)"""
166
+ import glob
167
+
168
+ if self._mapper_locations_loaded:
169
+ return
170
+ self._mapper_locations_loaded = True
171
+
172
+ for location in self.configuration.mapper_locations:
173
+ # 如果是目录,递归查找所有XML文件
174
+ if os.path.isdir(location):
175
+ xml_files = glob.glob(os.path.join(location, '**', '*.xml'), recursive=True)
176
+ elif location.endswith('.xml'):
177
+ xml_files = [location]
178
+ else:
179
+ # 尝试通配符匹配
180
+ xml_files = glob.glob(location)
181
+
182
+ for xml_file in sorted(set(xml_files)):
183
+ parser = XmlParser()
184
+ parser.parse_file(xml_file)
185
+
186
+ # 收集映射语句
187
+ for statement in parser.get_all_mapped_statements():
188
+ key = f"{parser.get_namespace()}.{statement.id}" if parser.get_namespace() else statement.id
189
+ if not self._statement_matches_database(statement):
190
+ continue
191
+ current = self.mapped_statements.get(key)
192
+ # A database-specific statement wins over the generic
193
+ # variant; a later generic statement never hides a
194
+ # matching vendor-specific one.
195
+ if current is None or (
196
+ current.database_id is None and statement.database_id is not None
197
+ ):
198
+ self.mapped_statements[key] = statement
199
+
200
+ # 收集结果映射
201
+ for result_map in parser.get_all_result_maps():
202
+ key = f"{parser.get_namespace()}.{result_map.id}" if parser.get_namespace() else result_map.id
203
+ self.result_maps[key] = result_map
204
+
205
+ def _resolve_sql(self, sql_or_id: str) -> Tuple[str, Optional[str], Optional[str]]:
206
+ """
207
+ 解析SQL或statement_id
208
+
209
+ Args:
210
+ sql_or_id: SQL语句或statement_id
211
+
212
+ Returns:
213
+ (sql, result_map_id, statement_type)
214
+ """
215
+ # 如果包含空格,可能是SQL语句
216
+ if ' ' in sql_or_id or '\n' in sql_or_id:
217
+ return sql_or_id, None, None
218
+
219
+ # 尝试从映射语句中查找
220
+ statement = self._get_mapped_statement(sql_or_id)
221
+ if statement is not None:
222
+ # 从sql_or_id中提取namespace
223
+ namespace = sql_or_id.rsplit('.', 1)[0] if '.' in sql_or_id else None
224
+ # 构建完整的result_map key(带namespace)
225
+ result_map_key = f"{namespace}.{statement.result_map}" if namespace and statement.result_map else statement.result_map
226
+ return statement.sql, result_map_key, statement.sql_type
227
+
228
+ # 默认当作SQL处理
229
+ return sql_or_id, None, None
230
+
231
+ def _get_mapped_statement(self, statement_id: str) -> Optional[MappedStatement]:
232
+ if not self._mapper_locations_loaded:
233
+ self._load_mapper_locations()
234
+ return self.mapped_statements.get(statement_id)
235
+
236
+ def _statement_matches_database(self, statement: MappedStatement) -> bool:
237
+ """Check a MyBatis ``databaseId`` against the configured dialect."""
238
+ database_id = statement.database_id
239
+ if not database_id:
240
+ return True
241
+ dialect = self.configuration.dialect.lower()
242
+ aliases = {
243
+ 'postgres': 'postgresql',
244
+ 'psycopg': 'postgresql',
245
+ 'sqlite3': 'sqlite',
246
+ }
247
+ return str(database_id).lower() in {dialect, aliases.get(dialect, dialect)}
248
+
249
+ def get_mapped_statement(self, statement_id: str) -> Optional[MappedStatement]:
250
+ """Return XML metadata for an id without exposing the internal registry."""
251
+ return self._get_mapped_statement(statement_id)
252
+
253
+ def _process_sql(self, sql: str, params: Dict[str, Any]) -> Tuple[str, List[Any]]:
254
+ processed_sql, param_order = self.dynamic_sql_processor.process(sql, params)
255
+ return processed_sql, [self.type_handler_registry.to_database(value) for value in param_order]
256
+
257
+ @staticmethod
258
+ def _copy_cached_result(value: Any) -> Any:
259
+ """Do not let callers mutate a cache entry owned by the Session."""
260
+ try:
261
+ return copy.deepcopy(value)
262
+ except Exception:
263
+ return value
264
+
265
+ @staticmethod
266
+ def _get_statement_for_id(sql_or_id: str, statement_lookup) -> Optional[MappedStatement]:
267
+ if ' ' in sql_or_id or '\n' in sql_or_id:
268
+ return None
269
+ return statement_lookup(sql_or_id)
270
+
271
+ def get_connection(self):
272
+ """
273
+ 获取数据库连接
274
+
275
+ Returns:
276
+ 数据库连接对象
277
+ """
278
+ if self._closed:
279
+ raise RuntimeError("SqlSession 已关闭")
280
+
281
+ if self._current_pooled_conn is not None:
282
+ if self._current_pooled_conn.is_valid():
283
+ return self._current_connection
284
+ self.connection_pool.return_connection(self._current_pooled_conn)
285
+ self._current_pooled_conn = None
286
+ self._current_connection = None
287
+
288
+ pooled_conn = self.connection_pool.get_connection()
289
+ self._current_connection = pooled_conn.get_connection()
290
+ # 保存PooledConnection引用以便正确归还
291
+ self._current_pooled_conn = pooled_conn
292
+ return self._current_connection
293
+
294
+ def return_connection(self) -> None:
295
+ """归还数据库连接到连接池"""
296
+ if self._current_pooled_conn is not None:
297
+ self.connection_pool.return_connection(self._current_pooled_conn)
298
+ self._current_pooled_conn = None
299
+ self._current_connection = None
300
+
301
+ @property
302
+ def _in_transaction(self) -> bool:
303
+ return self._transaction_depth > 0
304
+
305
+ @property
306
+ def in_transaction(self) -> bool:
307
+ """Public transaction-state query used by Spring integrations."""
308
+ return self._in_transaction
309
+
310
+ @contextlib.contextmanager
311
+ def _suspended_transaction(self):
312
+ """Temporarily detach the current physical transaction.
313
+
314
+ The pooled connection is deliberately kept out of the pool while the
315
+ suspended branch runs. Returning it would expose an uncommitted
316
+ transaction to another request. A second connection is therefore
317
+ required for ``REQUIRES_NEW``/``NOT_SUPPORTED``.
318
+ """
319
+ state = (
320
+ self._current_connection,
321
+ self._current_pooled_conn,
322
+ self._transaction_depth,
323
+ self._transaction_rollback_only,
324
+ self._transaction_dirty,
325
+ self._transaction_flush_cache,
326
+ self._transaction_isolation,
327
+ )
328
+ self._current_connection = None
329
+ self._current_pooled_conn = None
330
+ self._transaction_depth = 0
331
+ self._transaction_rollback_only = False
332
+ self._transaction_dirty = False
333
+ self._transaction_flush_cache = False
334
+ self._transaction_isolation = None
335
+ try:
336
+ yield
337
+ finally:
338
+ # Release only the temporary branch connection; the outer one is
339
+ # restored without touching its transaction state.
340
+ self.return_connection()
341
+ (
342
+ self._current_connection,
343
+ self._current_pooled_conn,
344
+ self._transaction_depth,
345
+ self._transaction_rollback_only,
346
+ self._transaction_dirty,
347
+ self._transaction_flush_cache,
348
+ self._transaction_isolation,
349
+ ) = state
350
+
351
+ def _handle_write_success(self, connection: Any, flush_cache: bool = True) -> None:
352
+ if self._in_transaction:
353
+ self._transaction_dirty = True
354
+ self._transaction_flush_cache = self._transaction_flush_cache or flush_cache
355
+ return
356
+ connection.commit()
357
+ if flush_cache:
358
+ self.sql_cache.clear()
359
+
360
+ def _handle_write_error(self, connection: Any) -> None:
361
+ if self._in_transaction:
362
+ self._transaction_rollback_only = True
363
+ return
364
+ try:
365
+ connection.rollback()
366
+ except Exception:
367
+ logger.exception("SQL执行失败后回滚连接失败")
368
+
369
+ def _validate_sql(self, sql: str) -> None:
370
+ """
371
+ 验证SQL语句安全性
372
+
373
+ Args:
374
+ sql: SQL语句
375
+
376
+ Raises:
377
+ SecurityError: SQL语句不安全
378
+ """
379
+ # 检查DDL(只阻止DROP/TRUNCATE/ALTER等危险DDL,不阻止CREATE TABLE等)
380
+ if self.sql_injection_detector.is_ddl_blocked(sql):
381
+ raise SecurityError(f"DDL语句被阻止: {sql}")
382
+
383
+ def _apply_access_control(self, sql: str, params: Dict[str, Any]) -> str:
384
+ """
385
+ 应用访问控制条件
386
+
387
+ Args:
388
+ sql: SQL语句
389
+ params: 参数
390
+
391
+ Returns:
392
+ 添加访问控制条件后的SQL语句
393
+ """
394
+ if not self.configuration.access_control_enabled:
395
+ return sql
396
+
397
+ # 提取表名(简化实现)
398
+ table_name = self._extract_table_name(sql)
399
+
400
+ # 获取行级访问条件
401
+ condition = self.access_control.get_access_condition(table_name, 'SELECT', self._user_context)
402
+ if condition:
403
+ # 检查SQL是否已有WHERE子句
404
+ if 'WHERE' in sql.upper():
405
+ sql = f"{sql} AND {condition}"
406
+ else:
407
+ sql = f"{sql} WHERE {condition}"
408
+
409
+ return sql
410
+
411
+ def _extract_table_name(self, sql: str) -> str:
412
+ """
413
+ 从SQL语句中提取表名(简化实现)
414
+
415
+ Args:
416
+ sql: SQL语句
417
+
418
+ Returns:
419
+ 表名
420
+ """
421
+ import re
422
+ # 匹配FROM后面的表名
423
+ match = re.search(r'FROM\s+(\w+)', sql, re.IGNORECASE)
424
+ if match:
425
+ return match.group(1)
426
+ return ''
427
+
428
+ def execute(self, sql_or_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
429
+ """按映射声明或SQL首关键字分派到对应的执行方法。"""
430
+ resolved_sql, _, statement_type = self._resolve_sql(sql_or_id)
431
+ operation = (statement_type or resolved_sql.lstrip().split(None, 1)[0]).lower()
432
+ dispatch = {
433
+ 'select': self.select,
434
+ 'insert': self.insert,
435
+ 'update': self.update,
436
+ 'delete': self.delete,
437
+ }
438
+ executor = dispatch.get(operation)
439
+ if executor is None:
440
+ raise ValueError(f"不支持的SQL操作: {operation or '<empty>'}")
441
+ return executor(sql_or_id, params)
442
+
443
+ def select(self, sql: str, params: Optional[Dict[str, Any]] = None,
444
+ result_map: Optional[str] = None,
445
+ use_cache: Optional[bool] = None,
446
+ fetch_size: Optional[int] = None,
447
+ timeout: Optional[int] = None,
448
+ _intercepted: bool = False) -> List[Dict[str, Any]]:
449
+ """
450
+ 执行SELECT查询
451
+
452
+ Args:
453
+ sql: SQL语句或statement_id
454
+ params: 参数字典
455
+ result_map: 结果映射ID
456
+
457
+ Returns:
458
+ 查询结果列表
459
+ """
460
+ params = params or {}
461
+ if not _intercepted and self.interceptor_chain.interceptors:
462
+ return self.interceptor_chain.invoke(
463
+ self, 'select', (sql, params),
464
+ {
465
+ 'result_map': result_map, 'use_cache': use_cache,
466
+ 'fetch_size': fetch_size, 'timeout': timeout,
467
+ },
468
+ lambda: self.select(
469
+ sql, params, result_map, use_cache, fetch_size, timeout,
470
+ _intercepted=True,
471
+ ),
472
+ )
473
+
474
+ statement = self._get_statement_for_id(sql, self._get_mapped_statement)
475
+
476
+ # 解析SQL或statement_id
477
+ processed_sql, stmt_result_map, _ = self._resolve_sql(sql)
478
+ result_map = result_map or stmt_result_map
479
+ if statement is not None:
480
+ fetch_size = fetch_size if fetch_size is not None else statement.fetch_size
481
+ timeout = timeout if timeout is not None else statement.timeout
482
+ if use_cache is None:
483
+ use_cache = statement.use_cache
484
+ if statement.flush_cache:
485
+ self.sql_cache.clear()
486
+ cache_enabled = self.configuration.cache_enabled if use_cache is None else use_cache
487
+ cache_params = dict(params)
488
+ if result_map:
489
+ cache_params['__pymybatis_result_map__'] = result_map
490
+
491
+ # 检查SQL注入
492
+ if self.configuration.sql_injection_detection:
493
+ for value in params.values():
494
+ if not self.sql_injection_detector.is_safe(value):
495
+ raise SecurityError(f"SQL注入检测失败: {value}")
496
+
497
+ # 处理动态SQL
498
+ processed_sql, param_order = self._process_sql(processed_sql, params)
499
+
500
+ # 验证SQL安全性
501
+ self._validate_sql(processed_sql)
502
+
503
+ # 应用访问控制
504
+ processed_sql = self._apply_access_control(processed_sql, params)
505
+
506
+ # 检查缓存
507
+ if cache_enabled:
508
+ cached_result = self.sql_cache.get(processed_sql, cache_params)
509
+ if cached_result is not None:
510
+ logger.debug(f"缓存命中: {processed_sql}")
511
+ return self._copy_cached_result(cached_result)
512
+
513
+ # 获取连接
514
+ connection = self.get_connection()
515
+
516
+ # 使用预编译缓存
517
+ cursor = None
518
+ try:
519
+ cursor = connection.cursor()
520
+ self._configure_cursor(cursor, fetch_size=fetch_size, timeout=timeout)
521
+
522
+ # 尝试从预编译缓存获取
523
+ cached_stmt = None
524
+ if self.configuration.sql_precompile_cache:
525
+ cached_stmt = GLOBAL_PRECOMPILED_CACHE.get(processed_sql)
526
+
527
+ if cached_stmt:
528
+ cursor.execute(cached_stmt, tuple(param_order))
529
+ else:
530
+ cursor.execute(processed_sql, tuple(param_order))
531
+ # 缓存预编译语句
532
+ if self.configuration.sql_precompile_cache:
533
+ GLOBAL_PRECOMPILED_CACHE.put(processed_sql, processed_sql)
534
+
535
+ # 获取结果
536
+ results = cursor.fetchall()
537
+
538
+ # 将结果转换为字典
539
+ if results:
540
+ if hasattr(cursor, 'description') and cursor.description:
541
+ columns = [desc[0] for desc in cursor.description]
542
+ results = [
543
+ dict(row) if isinstance(row, Mapping) or hasattr(row, 'keys')
544
+ else dict(zip(columns, row))
545
+ for row in results
546
+ ]
547
+
548
+ # 应用结果映射
549
+ result_map_obj = self.result_maps.get(result_map) if result_map else None
550
+ if result_map_obj is not None:
551
+ results = self._apply_result_map(results, result_map_obj)
552
+ if result_map_obj.type:
553
+ results = self._apply_statement_result_type(
554
+ results, result_map_obj.type
555
+ )
556
+
557
+ # 脱敏处理
558
+ if self.configuration.sensitive_data_masking:
559
+ results = self.sensitive_data_masker.mask_list(results)
560
+
561
+ if statement is not None and statement.result_type:
562
+ results = self._apply_statement_result_type(
563
+ results, statement.result_type
564
+ )
565
+
566
+ # 缓存结果
567
+ if cache_enabled:
568
+ self.sql_cache.put(
569
+ processed_sql, cache_params, self._copy_cached_result(results)
570
+ )
571
+
572
+ return results
573
+
574
+ finally:
575
+ if cursor:
576
+ cursor.close()
577
+
578
+ def select_one(self, sql: str, params: Optional[Dict[str, Any]] = None,
579
+ result_map: Optional[str] = None,
580
+ use_cache: Optional[bool] = None,
581
+ fetch_size: Optional[int] = None,
582
+ timeout: Optional[int] = None) -> Optional[Any]:
583
+ """
584
+ 执行SELECT查询,返回单条记录
585
+
586
+ Args:
587
+ sql: SQL语句或statement_id
588
+ params: 参数字典
589
+ result_map: 结果映射ID
590
+
591
+ Returns:
592
+ 查询结果,未找到返回None。如果只有一个字段,返回标量值。
593
+ """
594
+ results = self.select(
595
+ sql, params, result_map, use_cache, fetch_size, timeout
596
+ )
597
+ if not results:
598
+ return None
599
+
600
+ result = results[0]
601
+ # 如果结果只有一个字段,返回标量值(如COUNT查询)
602
+ if isinstance(result, dict) and len(result) == 1:
603
+ return list(result.values())[0]
604
+ return result
605
+
606
+ def select_pagination(self, sql: str, params: Optional[Dict[str, Any]] = None,
607
+ page_num: int = 1, page_size: int = 10) -> Dict[str, Any]:
608
+ """
609
+ 执行分页查询
610
+
611
+ Args:
612
+ sql: SQL语句或statement_id
613
+ params: 参数字典
614
+ page_num: 页码(从1开始)
615
+ page_size: 每页条数
616
+
617
+ Returns:
618
+ 分页结果,包含total和data
619
+ """
620
+ if page_num < 1:
621
+ raise ValueError("page_num 必须大于等于 1")
622
+ if page_size < 1 or page_size > self.configuration.max_batch_size:
623
+ raise ValueError(
624
+ f"page_size 必须在 1 到 {self.configuration.max_batch_size} 之间"
625
+ )
626
+
627
+ # 计算偏移量
628
+ offset = (page_num - 1) * page_size
629
+
630
+ # 检查偏移量限制
631
+ if offset > self.configuration.max_pagination_offset:
632
+ raise SecurityError(
633
+ f"分页偏移量({offset})超过最大限制({self.configuration.max_pagination_offset}),"
634
+ "请使用游标分页"
635
+ )
636
+
637
+ # 解析SQL
638
+ resolved_sql, _, _ = self._resolve_sql(sql)
639
+
640
+ # 构建分页SQL
641
+ pagination_sql = self.dialect.get_pagination_sql(resolved_sql, offset, page_size)
642
+
643
+ # 执行分页查询
644
+ data = self.select(pagination_sql, params)
645
+
646
+ # 计算总数(如果需要)
647
+ count_sql = f"SELECT COUNT(*) as total FROM ({resolved_sql}) t"
648
+ count_result = self.select_one(count_sql, params)
649
+ if isinstance(count_result, dict):
650
+ total = count_result.get('total', 0)
651
+ else:
652
+ total = count_result or 0
653
+
654
+ return {
655
+ 'total': total,
656
+ 'page_num': page_num,
657
+ 'page_size': page_size,
658
+ 'data': data
659
+ }
660
+
661
+ def select_cursor(self, sql: str, params: Optional[Dict[str, Any]] = None,
662
+ cursor_key: str = 'id', cursor_value: Optional[int] = None,
663
+ page_size: int = 100) -> Dict[str, Any]:
664
+ """
665
+ 执行游标分页查询(避免大偏移量分页)
666
+
667
+ Args:
668
+ sql: SQL语句或statement_id
669
+ params: 参数字典
670
+ cursor_key: 游标字段(通常为主键)
671
+ cursor_value: 游标值(上一页最后一条记录的cursor_key值)
672
+ page_size: 每页条数
673
+
674
+ Returns:
675
+ 分页结果,包含data和next_cursor
676
+ """
677
+ params = params or {}
678
+ if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?', cursor_key):
679
+ raise ValueError("cursor_key 不是合法的字段标识符")
680
+ if page_size < 1 or page_size > self.configuration.max_batch_size:
681
+ raise ValueError(
682
+ f"page_size 必须在 1 到 {self.configuration.max_batch_size} 之间"
683
+ )
684
+
685
+ # 解析SQL
686
+ resolved_sql, _, _ = self._resolve_sql(sql)
687
+
688
+ # 提取ORDER BY和LIMIT子句(如果存在)
689
+ order_by_clause = ''
690
+ remaining_sql = resolved_sql.rstrip().rstrip(';')
691
+
692
+ # 调用方传入的LIMIT会被统一替换,防止重复LIMIT或绕过页大小限制。
693
+ remaining_sql = re.sub(
694
+ r'\s+LIMIT\s+\d+(?:\s+OFFSET\s+\d+)?\s*$',
695
+ '',
696
+ remaining_sql,
697
+ flags=re.IGNORECASE,
698
+ )
699
+
700
+ # 处理ORDER BY
701
+ order_by_match = re.search(r'\s+ORDER\s+BY\s+.+$', remaining_sql, re.IGNORECASE)
702
+ if order_by_match:
703
+ order_by_clause = order_by_match.group(0)
704
+ remaining_sql = remaining_sql[:order_by_match.start()]
705
+
706
+ # 构建游标分页SQL
707
+ if cursor_value is not None:
708
+ # 检查SQL是否已有WHERE子句
709
+ if 'WHERE' in remaining_sql.upper():
710
+ cursor_sql = f"{remaining_sql} AND {cursor_key} > #{{_cursor_value}}"
711
+ else:
712
+ cursor_sql = f"{remaining_sql} WHERE {cursor_key} > #{{_cursor_value}}"
713
+
714
+ # 添加排序和限制
715
+ if not order_by_clause:
716
+ order_by_clause = f" ORDER BY {cursor_key}"
717
+ cursor_sql = f"{cursor_sql}{order_by_clause} LIMIT {page_size}"
718
+
719
+ # 添加游标参数
720
+ params = {**params, '_cursor_value': cursor_value}
721
+ else:
722
+ # 第一页
723
+ if not order_by_clause:
724
+ order_by_clause = f" ORDER BY {cursor_key}"
725
+ cursor_sql = f"{remaining_sql}{order_by_clause} LIMIT {page_size}"
726
+
727
+ # 执行查询
728
+ data = self.select(cursor_sql, params)
729
+
730
+ # 计算下一页游标
731
+ next_cursor = None
732
+ if len(data) == page_size:
733
+ next_cursor = data[-1].get(cursor_key.split('.')[-1])
734
+
735
+ return {
736
+ 'data': data,
737
+ 'next_cursor': next_cursor,
738
+ 'page_size': page_size
739
+ }
740
+
741
+ def cursor_page(self, sql: str, cursor_key: str = 'id',
742
+ cursor_value: Optional[int] = None, page_size: int = 100) -> Dict[str, Any]:
743
+ """
744
+ 执行游标分页查询(简化接口)
745
+
746
+ Args:
747
+ sql: SQL语句
748
+ cursor_key: 游标字段(通常为主键)
749
+ cursor_value: 游标值(上一页最后一条记录的cursor_key值)
750
+ page_size: 每页条数
751
+
752
+ Returns:
753
+ 分页结果,包含data和next_cursor
754
+ """
755
+ return self.select_cursor(sql, {}, cursor_key, cursor_value, page_size)
756
+
757
+ def insert(self, sql: str, params: Optional[Dict[str, Any]] = None,
758
+ use_generated_keys: bool = False,
759
+ timeout: Optional[int] = None,
760
+ _intercepted: bool = False) -> int:
761
+ """
762
+ 执行INSERT操作
763
+
764
+ Args:
765
+ sql: SQL语句或statement_id
766
+ params: 参数字典
767
+
768
+ Returns:
769
+ 影响的行数或自增主键
770
+ """
771
+ params = params or {}
772
+ if not _intercepted and self.interceptor_chain.interceptors:
773
+ return self.interceptor_chain.invoke(
774
+ self, 'insert', (sql, params),
775
+ {'use_generated_keys': use_generated_keys, 'timeout': timeout},
776
+ lambda: self.insert(
777
+ sql, params, use_generated_keys, timeout, _intercepted=True
778
+ ),
779
+ )
780
+
781
+ statement = self._get_statement_for_id(sql, self._get_mapped_statement)
782
+
783
+ # 解析SQL或statement_id
784
+ processed_sql, _, _ = self._resolve_sql(sql)
785
+ flush_cache = statement.flush_cache if statement is not None else True
786
+ if statement is not None:
787
+ timeout = timeout if timeout is not None else statement.timeout
788
+ use_generated_keys = use_generated_keys or statement.use_generated_keys
789
+
790
+ if statement.select_key_sql and statement.select_key_order == 'BEFORE':
791
+ key_value = self._execute_select_key(statement, params)
792
+ self._assign_key_to_params(
793
+ params,
794
+ statement.select_key_key_property or statement.key_property,
795
+ key_value,
796
+ )
797
+
798
+ # 检查SQL注入
799
+ if self.configuration.sql_injection_detection:
800
+ for value in params.values():
801
+ if not self.sql_injection_detector.is_safe(value):
802
+ raise SecurityError(f"SQL注入检测失败: {value}")
803
+
804
+ # 处理动态SQL
805
+ processed_sql, param_order = self._process_sql(processed_sql, params)
806
+
807
+ # 验证SQL安全性
808
+ self._validate_sql(processed_sql)
809
+
810
+ # 获取连接
811
+ connection = self.get_connection()
812
+ cursor = None
813
+
814
+ try:
815
+ # 执行SQL
816
+ cursor = connection.cursor()
817
+ self._configure_cursor(cursor, timeout=timeout)
818
+ cursor.execute(processed_sql, tuple(param_order))
819
+ affected_rows = cursor.rowcount
820
+ result_value = affected_rows
821
+
822
+ # 获取自增主键
823
+ if use_generated_keys and getattr(cursor, 'lastrowid', None) is not None:
824
+ result_value = cursor.lastrowid
825
+ elif use_generated_keys and self.dialect.get_dialect_name() == 'mysql':
826
+ cursor.execute("SELECT LAST_INSERT_ID()")
827
+ result = cursor.fetchone()
828
+ if result:
829
+ result_value = list(result.values())[0] if isinstance(result, dict) else result[0]
830
+
831
+ if use_generated_keys and statement and statement.key_property:
832
+ self._assign_key_to_params(params, statement.key_property, result_value)
833
+
834
+ self._handle_write_success(connection, flush_cache=flush_cache)
835
+
836
+ if statement is not None and statement.select_key_sql \
837
+ and statement.select_key_order == 'AFTER':
838
+ key_value = self._execute_select_key(statement, params)
839
+ self._assign_key_to_params(
840
+ params,
841
+ statement.select_key_key_property or statement.key_property,
842
+ key_value,
843
+ )
844
+ return result_value
845
+
846
+ except Exception:
847
+ self._handle_write_error(connection)
848
+ raise
849
+
850
+ finally:
851
+ if cursor:
852
+ cursor.close()
853
+
854
+ def _execute_select_key(
855
+ self, statement: MappedStatement, params: Dict[str, Any]
856
+ ) -> Any:
857
+ """Execute an XML ``selectKey`` and return its key value."""
858
+ rows = self.select(
859
+ statement.select_key_sql or '',
860
+ params,
861
+ use_cache=False,
862
+ timeout=statement.timeout,
863
+ )
864
+ if not rows:
865
+ return None
866
+ value = rows[0]
867
+ if isinstance(value, Mapping):
868
+ key_column = statement.select_key_key_column or statement.key_column
869
+ if key_column and key_column in value:
870
+ value = value[key_column]
871
+ elif len(value) == 1:
872
+ value = next(iter(value.values()))
873
+ result_type = statement.select_key_result_type
874
+ if result_type:
875
+ converted = self._apply_statement_result_type([value], result_type)
876
+ value = converted[0]
877
+ return value
878
+
879
+ def update(self, sql: str, params: Optional[Dict[str, Any]] = None,
880
+ timeout: Optional[int] = None,
881
+ _intercepted: bool = False) -> int:
882
+ """
883
+ 执行UPDATE操作
884
+
885
+ Args:
886
+ sql: SQL语句或statement_id
887
+ params: 参数字典
888
+
889
+ Returns:
890
+ 影响的行数
891
+ """
892
+ params = params or {}
893
+ if not _intercepted and self.interceptor_chain.interceptors:
894
+ return self.interceptor_chain.invoke(
895
+ self, 'update', (sql, params), {'timeout': timeout},
896
+ lambda: self.update(sql, params, timeout, _intercepted=True),
897
+ )
898
+
899
+ statement = self._get_statement_for_id(sql, self._get_mapped_statement)
900
+
901
+ # 解析SQL或statement_id
902
+ processed_sql, _, _ = self._resolve_sql(sql)
903
+ flush_cache = statement.flush_cache if statement is not None else True
904
+ if statement is not None:
905
+ timeout = timeout if timeout is not None else statement.timeout
906
+
907
+ # 检查SQL注入
908
+ if self.configuration.sql_injection_detection:
909
+ for value in params.values():
910
+ if not self.sql_injection_detector.is_safe(value):
911
+ raise SecurityError(f"SQL注入检测失败: {value}")
912
+
913
+ # 处理动态SQL
914
+ processed_sql, param_order = self._process_sql(processed_sql, params)
915
+
916
+ # 验证SQL安全性
917
+ self._validate_sql(processed_sql)
918
+
919
+ # 获取连接
920
+ connection = self.get_connection()
921
+ cursor = None
922
+
923
+ try:
924
+ cursor = connection.cursor()
925
+ self._configure_cursor(cursor, timeout=timeout)
926
+ cursor.execute(processed_sql, tuple(param_order))
927
+
928
+ self._handle_write_success(connection, flush_cache=flush_cache)
929
+
930
+ return cursor.rowcount
931
+
932
+ except Exception:
933
+ self._handle_write_error(connection)
934
+ raise
935
+
936
+ finally:
937
+ if cursor:
938
+ cursor.close()
939
+
940
+ def delete(self, sql: str, params: Optional[Dict[str, Any]] = None,
941
+ timeout: Optional[int] = None,
942
+ _intercepted: bool = False) -> int:
943
+ """
944
+ 执行DELETE操作
945
+
946
+ Args:
947
+ sql: SQL语句或statement_id
948
+ params: 参数字典
949
+
950
+ Returns:
951
+ 影响的行数
952
+ """
953
+ params = params or {}
954
+ if not _intercepted and self.interceptor_chain.interceptors:
955
+ return self.interceptor_chain.invoke(
956
+ self, 'delete', (sql, params), {'timeout': timeout},
957
+ lambda: self.delete(sql, params, timeout, _intercepted=True),
958
+ )
959
+
960
+ statement = self._get_statement_for_id(sql, self._get_mapped_statement)
961
+
962
+ # 解析SQL或statement_id
963
+ processed_sql, _, _ = self._resolve_sql(sql)
964
+ flush_cache = statement.flush_cache if statement is not None else True
965
+ if statement is not None:
966
+ timeout = timeout if timeout is not None else statement.timeout
967
+
968
+ # 检查SQL注入
969
+ if self.configuration.sql_injection_detection:
970
+ for value in params.values():
971
+ if not self.sql_injection_detector.is_safe(value):
972
+ raise SecurityError(f"SQL注入检测失败: {value}")
973
+
974
+ # 处理动态SQL
975
+ processed_sql, param_order = self._process_sql(processed_sql, params)
976
+
977
+ # 验证SQL安全性
978
+ self._validate_sql(processed_sql)
979
+
980
+ # 获取连接
981
+ connection = self.get_connection()
982
+ cursor = None
983
+
984
+ try:
985
+ cursor = connection.cursor()
986
+ self._configure_cursor(cursor, timeout=timeout)
987
+ cursor.execute(processed_sql, tuple(param_order))
988
+
989
+ self._handle_write_success(connection, flush_cache=flush_cache)
990
+
991
+ return cursor.rowcount
992
+
993
+ except Exception:
994
+ self._handle_write_error(connection)
995
+ raise
996
+
997
+ finally:
998
+ if cursor:
999
+ cursor.close()
1000
+
1001
+ @staticmethod
1002
+ def _configure_cursor(cursor: Any, fetch_size: Optional[int] = None,
1003
+ timeout: Optional[int] = None) -> None:
1004
+ if fetch_size is not None:
1005
+ if fetch_size <= 0:
1006
+ raise ValueError("fetch_size 必须大于0")
1007
+ cursor.arraysize = fetch_size
1008
+ if timeout is not None:
1009
+ if timeout <= 0:
1010
+ raise ValueError("timeout 必须大于0")
1011
+ if hasattr(cursor, 'timeout'):
1012
+ cursor.timeout = timeout
1013
+
1014
+ def _apply_result_map(self, results: List[Dict[str, Any]], result_map: ResultMap) -> List[Dict[str, Any]]:
1015
+ """
1016
+ 应用结果映射,将数据库列名转换为对象属性名
1017
+
1018
+ Args:
1019
+ results: 查询结果列表
1020
+ result_map: 结果映射
1021
+
1022
+ Returns:
1023
+ 映射后的结果列表
1024
+ """
1025
+ if not results:
1026
+ return results
1027
+
1028
+ mapped_results = []
1029
+ for row in results:
1030
+ mapped_results.append(self._map_result_row(row, result_map))
1031
+ return mapped_results
1032
+
1033
+ def _map_result_row(self, row: Mapping, result_map: ResultMap) -> Dict[str, Any]:
1034
+ """Map one joined row, including nested MyBatis result mappings."""
1035
+ selected_map = result_map
1036
+ if result_map.discriminator is not None:
1037
+ discriminator_value = row.get(result_map.discriminator.column)
1038
+ case_id = result_map.discriminator.cases.get(str(discriminator_value))
1039
+ if case_id:
1040
+ selected_map = self._lookup_result_map(case_id) or result_map
1041
+
1042
+ mapped_row: Dict[str, Any] = {}
1043
+ for column, value in row.items():
1044
+ property_name = selected_map.get_property(column)
1045
+ mapped_row[property_name or column] = value
1046
+
1047
+ for nested in selected_map.associations:
1048
+ value = self._load_nested_result(row, nested, collection=False)
1049
+ if value is not None:
1050
+ mapped_row[nested.property] = value
1051
+ for nested in selected_map.collections:
1052
+ value = self._load_nested_result(row, nested, collection=True)
1053
+ mapped_row[nested.property] = value
1054
+ return mapped_row
1055
+
1056
+ def _lookup_result_map(self, result_map_id: Optional[str]) -> Optional[ResultMap]:
1057
+ if not result_map_id:
1058
+ return None
1059
+ direct = self.result_maps.get(result_map_id)
1060
+ if direct is not None:
1061
+ return direct
1062
+ namespace = result_map_id.rsplit('.', 1)[0] if '.' in result_map_id else None
1063
+ if namespace:
1064
+ return self.result_maps.get(result_map_id)
1065
+ # Inline nested maps are registered under their generic id and under
1066
+ # the mapper namespace during XML loading.
1067
+ matches = [value for key, value in self.result_maps.items()
1068
+ if key.endswith('.' + result_map_id)]
1069
+ return matches[0] if matches else None
1070
+
1071
+ @staticmethod
1072
+ def _nested_parameters(row: Mapping, column: Optional[str]) -> Dict[str, Any]:
1073
+ if not column:
1074
+ return dict(row)
1075
+ # MyBatis supports composite columns: {id=author_id,type=kind}.
1076
+ if column.startswith('{') and column.endswith('}'):
1077
+ parameters: Dict[str, Any] = {}
1078
+ for item in column[1:-1].split(','):
1079
+ name, _, source = item.partition('=')
1080
+ if name and source:
1081
+ parameters[name.strip()] = row.get(source.strip())
1082
+ return parameters
1083
+ value = row.get(column)
1084
+ return {column: value, '_parameter': value}
1085
+
1086
+ def _load_nested_result(
1087
+ self, row: Mapping, nested: Any, collection: bool
1088
+ ) -> Any:
1089
+ params = self._nested_parameters(row, nested.column)
1090
+ if nested.select:
1091
+ nested_statement = nested.select
1092
+ if '.' not in nested_statement:
1093
+ # Resolve a relative statement id when XML used a namespace.
1094
+ candidates = [key for key in self.mapped_statements
1095
+ if key.endswith('.' + nested_statement)]
1096
+ nested_statement = candidates[0] if candidates else nested_statement
1097
+ if collection:
1098
+ return self.select(nested_statement, params)
1099
+ return self.select_one(nested_statement, params)
1100
+
1101
+ child_map = self._lookup_result_map(nested.result_map)
1102
+ if child_map is None:
1103
+ return None
1104
+ # Joined nested objects with no non-null mapped column represent SQL
1105
+ # NULL on the outer join and should remain None.
1106
+ mapped_columns = child_map.mappings.keys()
1107
+ if not any(row.get(column) is not None for column in mapped_columns):
1108
+ return [] if collection else None
1109
+ child = self._map_result_row(row, child_map)
1110
+ target_type = nested.result_type or nested.java_type or nested.of_type or child_map.type
1111
+ if target_type:
1112
+ child = self._apply_statement_result_type([child], target_type)[0]
1113
+ return [child] if collection else child
1114
+
1115
+ @staticmethod
1116
+ def _resolve_statement_result_type(result_type: str) -> Optional[Type]:
1117
+ builtins = {
1118
+ 'int': int,
1119
+ 'float': float,
1120
+ 'str': str,
1121
+ 'bool': bool,
1122
+ 'dict': dict,
1123
+ 'builtins.int': int,
1124
+ 'builtins.float': float,
1125
+ 'builtins.str': str,
1126
+ 'builtins.bool': bool,
1127
+ 'builtins.dict': dict,
1128
+ }
1129
+ if result_type in builtins:
1130
+ return builtins[result_type]
1131
+ if '.' not in result_type:
1132
+ return None
1133
+ module_name, type_name = result_type.rsplit('.', 1)
1134
+ return getattr(importlib.import_module(module_name), type_name)
1135
+
1136
+ def _apply_statement_result_type(self, results: List[Any], result_type: str) -> List[Any]:
1137
+ """Apply XML ``resultType`` when it can be resolved unambiguously.
1138
+
1139
+ Simple aliases match MyBatis' scalar behavior. Custom classes must be
1140
+ fully qualified; an unqualified name is intentionally left as a dict so
1141
+ mapper XML does not depend on process-wide import heuristics.
1142
+ """
1143
+ target_type = self._resolve_statement_result_type(result_type)
1144
+ if target_type is None:
1145
+ return results
1146
+
1147
+ def convert(value: Any) -> Any:
1148
+ if isinstance(value, target_type):
1149
+ return value
1150
+ if isinstance(value, Mapping):
1151
+ if target_type is dict:
1152
+ return dict(value)
1153
+ if len(value) == 1 and target_type in {int, float, str, bool}:
1154
+ return target_type(next(iter(value.values())))
1155
+ return target_type(**value)
1156
+ return target_type(value)
1157
+
1158
+ return [convert(value) for value in results]
1159
+
1160
+ @staticmethod
1161
+ def _assign_key_to_params(params: Dict[str, Any], property_name: str, value: Any) -> None:
1162
+ """Update a parameter mapping for XML ``keyProperty`` when possible."""
1163
+ if not property_name:
1164
+ return
1165
+ if '.' not in property_name:
1166
+ params[property_name] = value
1167
+ return
1168
+
1169
+ root, path = property_name.split('.', 1)
1170
+ target = params.get(root)
1171
+ if target is None:
1172
+ return
1173
+ parts = path.split('.')
1174
+ for part in parts[:-1]:
1175
+ if isinstance(target, Mapping):
1176
+ target = target.get(part)
1177
+ else:
1178
+ target = getattr(target, part, None)
1179
+ if target is None:
1180
+ return
1181
+ if isinstance(target, dict):
1182
+ target[parts[-1]] = value
1183
+ elif target is not None:
1184
+ setattr(target, parts[-1], value)
1185
+
1186
+ @contextlib.contextmanager
1187
+ def transaction(
1188
+ self,
1189
+ isolation_level: Optional[Any] = None,
1190
+ propagation: str = 'REQUIRED',
1191
+ ):
1192
+ """
1193
+ 事务上下文管理器
1194
+
1195
+ Usage:
1196
+ with session.transaction():
1197
+ session.insert(...)
1198
+ session.update(...)
1199
+ """
1200
+ normalized_propagation = str(propagation or 'REQUIRED').upper()
1201
+ supported = {
1202
+ 'REQUIRED', 'REQUIRES_NEW', 'NESTED', 'SUPPORTS',
1203
+ 'MANDATORY', 'NOT_SUPPORTED', 'NEVER',
1204
+ }
1205
+ if normalized_propagation not in supported:
1206
+ raise ValueError(
1207
+ f"不支持的事务传播级别: {propagation}; 可选: {', '.join(sorted(supported))}"
1208
+ )
1209
+
1210
+ # Non-transactional propagation modes do not alter transaction state.
1211
+ if normalized_propagation == 'NEVER':
1212
+ if self._in_transaction:
1213
+ raise RuntimeError("事务传播 NEVER 要求当前不存在活动事务")
1214
+ yield
1215
+ return
1216
+ if normalized_propagation == 'MANDATORY' and not self._in_transaction:
1217
+ raise RuntimeError("事务传播 MANDATORY 要求当前已存在活动事务")
1218
+ if normalized_propagation == 'SUPPORTS' and not self._in_transaction:
1219
+ yield
1220
+ return
1221
+ if normalized_propagation == 'SUPPORTS':
1222
+ # Join the ambient transaction without creating a new logical
1223
+ # boundary. An exception is intentionally propagated to the
1224
+ # caller, which owns the rollback decision.
1225
+ yield
1226
+ return
1227
+ if normalized_propagation == 'NOT_SUPPORTED' and self._in_transaction:
1228
+ with self._suspended_transaction():
1229
+ yield
1230
+ return
1231
+ if normalized_propagation == 'NOT_SUPPORTED':
1232
+ yield
1233
+ return
1234
+ if normalized_propagation == 'REQUIRES_NEW' and self._in_transaction:
1235
+ with self._suspended_transaction():
1236
+ with self.transaction(
1237
+ isolation_level=isolation_level,
1238
+ propagation='REQUIRED',
1239
+ ):
1240
+ yield
1241
+ return
1242
+
1243
+ connection = self.get_connection()
1244
+ is_outermost = self._transaction_depth == 0
1245
+
1246
+ # ``NESTED`` uses a savepoint inside an existing physical transaction.
1247
+ # Unlike REQUIRED's rollback-only behavior, a handled nested failure can
1248
+ # leave the outer transaction usable.
1249
+ if normalized_propagation == 'NESTED' and not is_outermost:
1250
+ savepoint = f"pymybatis_nested_{self._transaction_depth}"
1251
+ cursor = connection.cursor()
1252
+ try:
1253
+ cursor.execute(f"SAVEPOINT {savepoint}")
1254
+ finally:
1255
+ cursor.close()
1256
+ self._transaction_depth += 1
1257
+ try:
1258
+ yield
1259
+ except Exception:
1260
+ cursor = connection.cursor()
1261
+ try:
1262
+ cursor.execute(f"ROLLBACK TO SAVEPOINT {savepoint}")
1263
+ cursor.execute(f"RELEASE SAVEPOINT {savepoint}")
1264
+ finally:
1265
+ cursor.close()
1266
+ raise
1267
+ else:
1268
+ cursor = connection.cursor()
1269
+ try:
1270
+ cursor.execute(f"RELEASE SAVEPOINT {savepoint}")
1271
+ finally:
1272
+ cursor.close()
1273
+ finally:
1274
+ self._transaction_depth -= 1
1275
+ return
1276
+
1277
+ requested_isolation = (
1278
+ self._transaction_isolation
1279
+ if not is_outermost and isolation_level is None
1280
+ else self._resolve_transaction_isolation(isolation_level)
1281
+ )
1282
+
1283
+ if is_outermost:
1284
+ self._set_transaction_isolation(connection, requested_isolation)
1285
+ if hasattr(connection, 'begin'):
1286
+ connection.begin()
1287
+ else:
1288
+ connection.execute('BEGIN')
1289
+ self._transaction_rollback_only = False
1290
+ self._transaction_dirty = False
1291
+ self._transaction_flush_cache = False
1292
+ self._transaction_isolation = requested_isolation
1293
+ elif requested_isolation != self._transaction_isolation:
1294
+ raise RuntimeError("嵌套事务不能更改隔离级别")
1295
+
1296
+ self._transaction_depth += 1
1297
+ try:
1298
+ yield
1299
+ except Exception:
1300
+ self._transaction_rollback_only = True
1301
+ if is_outermost:
1302
+ connection.rollback()
1303
+ raise
1304
+ else:
1305
+ if is_outermost:
1306
+ if self._transaction_rollback_only:
1307
+ connection.rollback()
1308
+ raise RuntimeError("事务已标记为仅回滚,不能提交")
1309
+ connection.commit()
1310
+ if self._transaction_dirty and self._transaction_flush_cache:
1311
+ self.sql_cache.clear()
1312
+ finally:
1313
+ self._transaction_depth -= 1
1314
+ if is_outermost:
1315
+ self._transaction_rollback_only = False
1316
+ self._transaction_dirty = False
1317
+ self._transaction_flush_cache = False
1318
+ self._transaction_isolation = None
1319
+
1320
+ def _resolve_transaction_isolation(
1321
+ self, isolation_level: Optional[Any]
1322
+ ) -> TransactionIsolationLevel:
1323
+ if isolation_level is None:
1324
+ return self.transaction_manager.default_isolation_level
1325
+ if isinstance(isolation_level, TransactionIsolationLevel):
1326
+ return isolation_level
1327
+ try:
1328
+ return TransactionIsolationLevel(str(isolation_level).upper())
1329
+ except ValueError as exc:
1330
+ supported = ', '.join(level.value for level in TransactionIsolationLevel)
1331
+ raise ValueError(f"不支持的事务隔离级别: {isolation_level}; 可选: {supported}") from exc
1332
+
1333
+ def _set_transaction_isolation(
1334
+ self, connection: Any, isolation_level: TransactionIsolationLevel
1335
+ ) -> None:
1336
+ if connection.__class__.__module__.startswith('sqlite3'):
1337
+ return
1338
+
1339
+ normalized = isolation_level.value.replace('_', ' ')
1340
+ set_session = getattr(connection, 'set_session', None)
1341
+ if callable(set_session):
1342
+ set_session(isolation_level=normalized)
1343
+ return
1344
+
1345
+ cursor = connection.cursor()
1346
+ try:
1347
+ cursor.execute(f"SET TRANSACTION ISOLATION LEVEL {normalized}")
1348
+ finally:
1349
+ cursor.close()
1350
+
1351
+ def get_mapper(self, mapper_class: Type) -> Any:
1352
+ """
1353
+ 获取Mapper代理对象
1354
+
1355
+ Args:
1356
+ mapper_class: Mapper类
1357
+
1358
+ Returns:
1359
+ Mapper代理对象
1360
+ """
1361
+ return MapperProxy(mapper_class, self)
1362
+
1363
+ def close(self) -> None:
1364
+ """关闭SqlSession"""
1365
+ if self._closed:
1366
+ return
1367
+ if self._in_transaction and self._current_connection is not None:
1368
+ self._current_connection.rollback()
1369
+ self._transaction_depth = 0
1370
+ self.return_connection()
1371
+ if self._owns_connection_pool:
1372
+ self.connection_pool.close()
1373
+ self._closed = True
1374
+
1375
+ def __enter__(self):
1376
+ """进入上下文管理器"""
1377
+ return self
1378
+
1379
+ def __exit__(self, exc_type, exc_val, exc_tb):
1380
+ """退出上下文管理器"""
1381
+ self.close()
1382
+ return False