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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- 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
|