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
spring/orm/ddl_auto.py ADDED
@@ -0,0 +1,1217 @@
1
+ """
2
+ ORM DDL 自动建表/更新 (JPA hibernate.ddl-auto 风格)
3
+
4
+ 支持从 Python 实体类(dataclass / 带类型注解的类)自动生成 DDL 语句,
5
+ 并根据配置执行建表/更新/验证。
6
+
7
+ 支持的 ddl-auto 模式:
8
+ - none: 不做任何操作(默认)
9
+ - validate: 验证表结构是否与实体匹配,不匹配时报错
10
+ - update: 增量更新表结构(添加新列、新索引)
11
+ - create: 每次启动都删除并重新创建表
12
+ - create-drop: 启动时创建,关闭时删除(测试用)
13
+
14
+ 支持 MySQL/PostgreSQL/SQLite
15
+ """
16
+
17
+ import logging
18
+ import inspect
19
+ import time
20
+ import threading
21
+ from typing import Dict, List, Optional, Any, Type, Tuple, get_type_hints
22
+ from dataclasses import is_dataclass, fields as dataclass_fields
23
+ from enum import Enum
24
+
25
+ from spring.core.typing_utils import unwrap_optional_type
26
+
27
+ logger = logging.getLogger("Spring.ORM.DDL")
28
+
29
+
30
+ class DdlAutoMode(Enum):
31
+ NONE = "none"
32
+ VALIDATE = "validate"
33
+ UPDATE = "update"
34
+ CREATE = "create"
35
+ CREATE_DROP = "create-drop"
36
+
37
+
38
+ # 类型映射: Python type -> (MySQL type, PostgreSQL type, SQLite type)
39
+ _TYPE_MAP = {
40
+ int: ("BIGINT", "BIGINT", "INTEGER"),
41
+ str: ("VARCHAR(255)", "VARCHAR(255)", "TEXT"),
42
+ float: ("DOUBLE", "DOUBLE PRECISION", "REAL"),
43
+ bool: ("TINYINT(1)", "BOOLEAN", "INTEGER"),
44
+ bytes: ("BLOB", "BYTEA", "BLOB"),
45
+ }
46
+
47
+
48
+ def _get_sql_type(py_type: Any, dialect: str, column_info: dict = None) -> str:
49
+ """将Python类型映射到SQL类型"""
50
+ column_info = column_info or {}
51
+ # 自定义长度
52
+ length = column_info.get('length')
53
+ if py_type is str and length:
54
+ if dialect == 'mysql':
55
+ return f"VARCHAR({length})"
56
+ elif dialect == 'postgresql':
57
+ return f"VARCHAR({length})"
58
+ else:
59
+ return "TEXT"
60
+ # 显式指定 columnDefinition
61
+ col_def = column_info.get('column_definition')
62
+ if col_def:
63
+ return col_def
64
+ mapped = _TYPE_MAP.get(py_type)
65
+ if mapped:
66
+ if dialect == 'mysql':
67
+ return mapped[0]
68
+ elif dialect == 'postgresql':
69
+ return mapped[1]
70
+ else:
71
+ return mapped[2]
72
+ # datetime / date
73
+ type_name = getattr(py_type, '__name__', str(py_type))
74
+ if 'datetime' in type_name.lower() or 'date' in type_name.lower():
75
+ if dialect == 'mysql':
76
+ return "DATETIME"
77
+ elif dialect == 'postgresql':
78
+ return "TIMESTAMP"
79
+ else:
80
+ return "TEXT"
81
+ if 'decimal' in type_name.lower():
82
+ precision = column_info.get('precision', 10)
83
+ scale = column_info.get('scale', 2)
84
+ if dialect in ('mysql', 'postgresql'):
85
+ return f"DECIMAL({precision},{scale})"
86
+ return "REAL"
87
+ # 默认:TEXT
88
+ return "TEXT" if dialect == 'sqlite' else ("VARCHAR(255)" if dialect in ('mysql', 'postgresql') else "TEXT")
89
+
90
+
91
+ class Column:
92
+ """列定义注解/描述符"""
93
+ def __init__(self, name: str = "", nullable: bool = True, unique: bool = False,
94
+ length: int = 0, primary_key: bool = False, auto_increment: bool = False,
95
+ default: Any = None, column_definition: str = "", comment: str = "",
96
+ precision: int = 0, scale: int = 0):
97
+ self.name = name
98
+ self.nullable = nullable
99
+ self.unique = unique
100
+ self.length = length
101
+ self.primary_key = primary_key
102
+ self.auto_increment = auto_increment
103
+ self.default = default
104
+ self.column_definition = column_definition
105
+ self.comment = comment
106
+ self.precision = precision
107
+ self.scale = scale
108
+
109
+
110
+ class Id(Column):
111
+ """主键列"""
112
+ def __init__(self, name: str = "", auto_increment: bool = True, **kwargs):
113
+ kwargs.pop('primary_key', None)
114
+ super().__init__(name=name, primary_key=True, auto_increment=auto_increment,
115
+ nullable=False, **kwargs)
116
+
117
+
118
+ class Version(Column):
119
+ """``@Version`` 乐观锁字段(对齐 JPA ``javax.persistence.Version``)。
120
+
121
+ 标记该字段为乐观锁版本号:DDL 生成 ``INTEGER NOT NULL DEFAULT 0``;
122
+ 更新时配合 ``OptimisticLockExecutor`` 在 WHERE 子句追加 ``version = ?`` 并自增。
123
+
124
+ 用法(两种形式,与 ``Column``/``Id`` 一致)::
125
+
126
+ @entity("sys_user")
127
+ class User:
128
+ id = Id()
129
+ version = Version() # 类属性描述符形式
130
+ def __init__(self, id=None, version=0): ...
131
+
132
+ # 或函数装饰器形式
133
+ @version_column()
134
+ def version(self): ...
135
+
136
+ 与 JPA 的差异(已标注):
137
+ - JPA/Hibernate 由 ORM 自动在 UPDATE 时追加 version 检查并自增;
138
+ 本框架内嵌 PyMyBatis 不自动注入 version 子句,需通过 ``OptimisticLockExecutor``
139
+ 显式执行乐观锁更新(见本文件末尾),或在 XML/注解 SQL 中手写 version 条件。
140
+ """
141
+ def __init__(self, name: str = "", **kwargs):
142
+ kwargs.pop('primary_key', None)
143
+ kwargs.pop('auto_increment', None)
144
+ # 版本字段非空、默认 0
145
+ kwargs.setdefault('nullable', False)
146
+ kwargs.setdefault('default', 0)
147
+ super().__init__(name=name, primary_key=False, auto_increment=False, **kwargs)
148
+ # 版本标记,供 _build_column_meta 识别
149
+ self.version = True
150
+
151
+
152
+ class Transient:
153
+ """``@Transient`` 瞬态字段标记(对齐 JPA ``javax.persistence.Transient``)。
154
+
155
+ 标记该字段**不持久化**:DDL 自动建表与实体解析均跳过该字段。
156
+
157
+ 用法(两种形式,与 ``ExcelIgnore`` 一致)::
158
+
159
+ @entity("sys_user")
160
+ class User:
161
+ id = Id()
162
+ display_name = Transient() # 类属性描述符形式:不落库
163
+ def __init__(self, id=None, display_name=None): ...
164
+
165
+ # 或函数装饰器形式
166
+ @transient_field()
167
+ def display_name(self): ...
168
+
169
+ 实现为独立标记类(非 ``Column`` 子类),因为瞬态字段根本不是列。
170
+ """
171
+ def __init__(self, default: Any = None):
172
+ self.default = default
173
+ self.attr_name: str = ""
174
+
175
+ def __set_name__(self, owner: type, name: str) -> None:
176
+ """类属性描述符形式时,Python 自动回填字段名(镜像 ``ExcelIgnore``)。"""
177
+ self.attr_name = name
178
+
179
+ def __call__(self, target):
180
+ """函数装饰器形式:``@Transient()``,把 ``__transient__`` 标记挂到目标。"""
181
+ setattr(target, '__transient__', True)
182
+ self.attr_name = getattr(target, '__name__', '')
183
+ return target
184
+
185
+
186
+ class Table:
187
+ """表注解"""
188
+ def __init__(self, name: str = "", indexes: List['Index'] = None, comment: str = ""):
189
+ self.name = name
190
+ self.indexes = indexes or []
191
+ self.comment = comment
192
+
193
+
194
+ class Index:
195
+ """索引定义"""
196
+ def __init__(self, name: str, columns: List[str], unique: bool = False):
197
+ self.name = name
198
+ self.columns = columns
199
+ self.unique = unique
200
+
201
+
202
+ def column(**kwargs):
203
+ """字段列装饰器/描述符,用于标注实体字段"""
204
+ col = Column(**kwargs)
205
+ def decorator(f):
206
+ setattr(f, '__column__', col)
207
+ return f
208
+ if len(kwargs) == 1 and 'name' in kwargs and callable(kwargs.get('name')):
209
+ # used as @column without parens
210
+ f = kwargs['name']
211
+ setattr(f, '__column__', Column())
212
+ return f
213
+ return decorator
214
+
215
+
216
+ def id_column(auto_increment: bool = True, **kwargs):
217
+ """主键列装饰器"""
218
+ col = Id(auto_increment=auto_increment, **kwargs)
219
+ def decorator(f):
220
+ setattr(f, '__column__', col)
221
+ return f
222
+ return decorator
223
+
224
+
225
+ def version_column(**kwargs):
226
+ """``@Version`` 函数装饰器形式(镜像 ``column()`` / ``id_column()``)。
227
+
228
+ 用法::
229
+
230
+ @entity("sys_user")
231
+ class User:
232
+ id = Id()
233
+ @version_column()
234
+ def version(self): ...
235
+ """
236
+ col = Version(**kwargs)
237
+ def decorator(f):
238
+ setattr(f, '__column__', col)
239
+ return f
240
+ return decorator
241
+
242
+
243
+ def transient_field():
244
+ """``@Transient`` 函数装饰器形式(镜像 ``ExcelIgnore.__call__``)。
245
+
246
+ 用法::
247
+
248
+ @entity("sys_user")
249
+ class User:
250
+ id = Id()
251
+ @transient_field()
252
+ def display_name(self): ...
253
+ """
254
+ def decorator(f):
255
+ setattr(f, '__transient__', True)
256
+ return f
257
+ return decorator
258
+
259
+
260
+ def _camel_to_snake(name: str) -> str:
261
+ """驼峰转下划线"""
262
+ import re
263
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
264
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
265
+
266
+
267
+ def _is_transient_field(cls: Type, attr_name: str) -> bool:
268
+ """检查字段是否标记为 ``@Transient``(对齐 Excel ``_has_explicit_properties`` 反射范式)。
269
+
270
+ 判定规则(取 MRO 中最近声明):
271
+ 1. 类属性为 ``Transient`` 实例(描述符形式)。
272
+ 2. 类属性带 ``__transient__`` 标记(``@transient_field()`` 函数装饰器形式)。
273
+ """
274
+ for cls_base in cls.__mro__:
275
+ if attr_name in cls_base.__dict__:
276
+ cval = cls_base.__dict__[attr_name]
277
+ if isinstance(cval, Transient):
278
+ return True
279
+ if getattr(cval, '__transient__', False) is True:
280
+ return True
281
+ return False # 最近声明非瞬态,子类覆盖
282
+ return False
283
+
284
+
285
+ class EntityTable:
286
+ """从实体类解析出的表元数据"""
287
+ __slots__ = ('table_name', 'columns', 'indexes', 'comment', 'entity_class')
288
+
289
+ def __init__(self, table_name: str, columns: List[dict], indexes: List[Index],
290
+ comment: str, entity_class: type):
291
+ self.table_name = table_name
292
+ self.columns = columns
293
+ self.indexes = indexes
294
+ self.comment = comment
295
+ self.entity_class = entity_class
296
+
297
+
298
+ class DdlAutoManager:
299
+ """
300
+ DDL 自动管理器
301
+
302
+ Usage:
303
+ ddl = DdlAutoManager(connection_pool, dialect="mysql", mode="update")
304
+ ddl.register_entity(User)
305
+ ddl.register_entity(Order)
306
+ ddl.execute() # 根据mode执行
307
+ """
308
+
309
+ def __init__(self, connection_pool, dialect: str = "mysql",
310
+ mode: str = "none", entity_packages: List[str] = None):
311
+ self.pool = connection_pool
312
+ self.dialect = dialect.lower()
313
+ try:
314
+ self.mode = DdlAutoMode(mode.lower())
315
+ except ValueError:
316
+ logger.warning(f"Unknown ddl-auto mode: {mode}, using 'none'")
317
+ self.mode = DdlAutoMode.NONE
318
+ self._entities: List[Type] = []
319
+ self._parsed: List[EntityTable] = []
320
+ self._executed_sql: List[str] = []
321
+ self._lock = threading.Lock()
322
+ if entity_packages:
323
+ self._scan_packages(entity_packages)
324
+
325
+ def register_entity(self, entity_class: Type):
326
+ """注册实体类"""
327
+ if entity_class not in self._entities:
328
+ self._entities.append(entity_class)
329
+
330
+ def register_entities(self, entity_classes: List[Type]):
331
+ """批量注册实体类"""
332
+ for cls in entity_classes:
333
+ self.register_entity(cls)
334
+
335
+ def _scan_packages(self, packages: List[str]):
336
+ """扫描包下所有实体类(简单实现:要求实体类使用@entity/@Table装饰器或继承基类)"""
337
+ import importlib
338
+ import pkgutil
339
+ for pkg_name in packages:
340
+ try:
341
+ pkg = importlib.import_module(pkg_name)
342
+ for importer, modname, ispkg in pkgutil.walk_packages(pkg.__path__, pkg_name + '.'):
343
+ try:
344
+ mod = importlib.import_module(modname)
345
+ for name, obj in inspect.getmembers(mod, inspect.isclass):
346
+ # 支持 @entity 装饰器标记
347
+ is_entity = getattr(obj, '__entity__', False)
348
+ has_table = hasattr(obj, '__table__')
349
+ has_tablename = hasattr(obj, '__tablename__') and not name.startswith('_')
350
+ if (is_entity or has_table or has_tablename) and obj.__module__ == modname:
351
+ self.register_entity(obj)
352
+ except Exception as e:
353
+ logger.debug(f"Failed to scan module {modname}: {e}")
354
+ except Exception as e:
355
+ logger.warning(f"Failed to scan package {pkg_name}: {e}")
356
+
357
+ def _parse_entity(self, cls: Type) -> EntityTable:
358
+ """解析实体类,提取表元数据"""
359
+ # 表名
360
+ table_meta = getattr(cls, '__table__', None)
361
+ table_name = ""
362
+ table_comment = ""
363
+ indexes = []
364
+ if isinstance(table_meta, Table):
365
+ table_name = table_meta.name
366
+ table_comment = table_meta.comment
367
+ indexes = list(table_meta.indexes)
368
+ if not table_name:
369
+ table_name = getattr(cls, '__tablename__', "")
370
+ if not table_name:
371
+ table_name = _camel_to_snake(cls.__name__)
372
+
373
+ columns = []
374
+
375
+ # 处理 dataclass
376
+ if is_dataclass(cls):
377
+ for df in dataclass_fields(cls):
378
+ # @Transient 字段不持久化,跳过(与普通类分支一致)
379
+ if _is_transient_field(cls, df.name):
380
+ continue
381
+ # dataclass 字段默认值若直接是 Transient 实例,也跳过
382
+ if isinstance(df.default, Transient):
383
+ continue
384
+ col_meta = self._get_field_meta(df)
385
+ columns.append(col_meta)
386
+ else:
387
+ # 处理普通类:从 __init__ 方法中提取 self.xxx 字段
388
+ init_fields = self._extract_init_fields(cls)
389
+ # 同时检查类级别的类型注解(__init__ 的参数注解)
390
+ init_hints = {}
391
+ try:
392
+ init_hints = get_type_hints(cls.__init__)
393
+ except Exception:
394
+ pass
395
+ cls_annotations = getattr(cls, '__annotations__', {})
396
+ for attr_name, default_val in init_fields.items():
397
+ # @Transient 字段不持久化,跳过
398
+ if _is_transient_field(cls, attr_name):
399
+ continue
400
+ # 优先取 __init__ 注解,其次取类注解
401
+ py_type = init_hints.get(attr_name) or cls_annotations.get(attr_name)
402
+ # 解包 Optional[X]:Python 3.10 的 get_type_hints 会把带 None 默认值的
403
+ # 参数注解自动包装为 Optional[X],3.11+ 不再包装。此处统一解包为承载类型,
404
+ # 否则 _get_sql_type 在 3.10 上识别失败回退到 TEXT,导致数值列被当字符串存储。
405
+ py_type = unwrap_optional_type(py_type)
406
+ if py_type is None:
407
+ py_type = type(default_val) if default_val is not None and default_val != "" else str
408
+ # 检查Column注解
409
+ col_info = None
410
+ for cls_base in cls.__mro__:
411
+ if attr_name in cls_base.__dict__:
412
+ cval = cls_base.__dict__[attr_name]
413
+ if isinstance(cval, Column):
414
+ col_info = cval
415
+ elif hasattr(cval, '__column__'):
416
+ col_info = getattr(cval, '__column__')
417
+ break
418
+ col_meta = self._build_column_meta(attr_name, py_type, col_info)
419
+ columns.append(col_meta)
420
+
421
+ # 检查是否已有主键标记,没有的话检查id字段并标记为PK
422
+ has_pk = any(c['primary_key'] for c in columns)
423
+ id_col = None
424
+ for c in columns:
425
+ if c['py_name'] == 'id' or c['name'] == 'id':
426
+ id_col = c
427
+ break
428
+
429
+ if not has_pk and id_col is not None:
430
+ # 将现有id字段标记为主键和自增
431
+ id_col['primary_key'] = True
432
+ id_col['auto_increment'] = True
433
+ id_col['nullable'] = False
434
+ if self.dialect == 'sqlite':
435
+ id_col['sql_type'] = 'INTEGER'
436
+ elif self.dialect == 'mysql':
437
+ id_col['sql_type'] = _get_sql_type(int, self.dialect)
438
+ elif self.dialect == 'postgresql':
439
+ id_col['sql_type'] = 'BIGSERIAL' if 'BIGINT' in id_col['sql_type'] else 'SERIAL'
440
+ elif not has_pk:
441
+ # 没有id字段,自动添加id主键
442
+ pk_col = {
443
+ 'name': 'id',
444
+ 'py_name': 'id',
445
+ 'py_type': int,
446
+ 'sql_type': _get_sql_type(int, self.dialect),
447
+ 'nullable': False,
448
+ 'unique': False,
449
+ 'primary_key': True,
450
+ 'auto_increment': True,
451
+ 'default': None,
452
+ 'comment': 'Primary key',
453
+ }
454
+ if self.dialect == 'sqlite':
455
+ pk_col['sql_type'] = 'INTEGER'
456
+ columns.insert(0, pk_col)
457
+
458
+ return EntityTable(table_name, columns, indexes, table_comment, cls)
459
+
460
+ def _extract_init_fields(self, cls: Type) -> Dict[str, Any]:
461
+ """从__init__方法中提取 self.xxx[: type] = default 字段"""
462
+ import textwrap
463
+ fields = {}
464
+ try:
465
+ source = inspect.getsource(cls.__init__)
466
+ source = textwrap.dedent(source)
467
+ import re
468
+ # 支持类型注解:self.xxx: type = value 或 self.xxx = value
469
+ for match in re.finditer(r'self\.(\w+)(?::\s*[\w\[\], .]+)?\s*=\s*([^\n#]+)', source):
470
+ fname = match.group(1)
471
+ val_expr = match.group(2).strip()
472
+ if fname.startswith('_'):
473
+ continue
474
+ val_expr = val_expr.rstrip(',').strip()
475
+ # 推断默认值
476
+ default = None
477
+ if val_expr in ('None',):
478
+ default = None
479
+ elif val_expr in ('""', "''", '""""""', "''''''"):
480
+ default = ""
481
+ elif val_expr in ('[]', 'list()'):
482
+ default = []
483
+ elif val_expr in ('{}', 'dict()'):
484
+ default = {}
485
+ elif val_expr == '0':
486
+ default = 0
487
+ elif val_expr in ('0.0', '0.'):
488
+ default = 0.0
489
+ elif val_expr == 'False':
490
+ default = False
491
+ elif val_expr == 'True':
492
+ default = True
493
+ else:
494
+ # self.param = param 模式 (参数赋值)
495
+ param_match = re.match(r'^(\w+)$', val_expr)
496
+ if param_match and param_match.group(1) == fname:
497
+ default = None
498
+ else:
499
+ try:
500
+ default = eval(val_expr, {"__builtins__": {}}, {})
501
+ except Exception:
502
+ default = None
503
+ fields[fname] = default if default is not None else ""
504
+ except Exception:
505
+ pass
506
+ return fields
507
+
508
+ def _get_field_meta(self, df) -> dict:
509
+ """从dataclass字段提取列元数据"""
510
+ col_info = getattr(df.default, '__column__', None) if df.default is not inspect.Parameter.empty else None
511
+ if col_info is None and isinstance(df.default, Column):
512
+ col_info = df.default
513
+ return self._build_column_meta(df.name, df.type, col_info)
514
+
515
+ def _build_column_meta(self, attr_name: str, py_type: Any, col_info: Optional[Column]) -> dict:
516
+ """构建列元数据字典"""
517
+ info = {
518
+ 'name': attr_name,
519
+ 'py_name': attr_name,
520
+ 'py_type': py_type,
521
+ 'nullable': True,
522
+ 'unique': False,
523
+ 'primary_key': False,
524
+ 'auto_increment': False,
525
+ 'default': None,
526
+ 'comment': '',
527
+ 'length': 0,
528
+ 'precision': 0,
529
+ 'scale': 0,
530
+ 'column_definition': '',
531
+ 'version': False, # @Version 乐观锁标记
532
+ }
533
+ if col_info and isinstance(col_info, Column):
534
+ if col_info.name:
535
+ info['name'] = col_info.name
536
+ else:
537
+ info['name'] = _camel_to_snake(attr_name)
538
+ info['nullable'] = col_info.nullable
539
+ info['unique'] = col_info.unique
540
+ info['primary_key'] = col_info.primary_key
541
+ info['auto_increment'] = col_info.auto_increment
542
+ info['default'] = col_info.default
543
+ info['comment'] = col_info.comment
544
+ info['length'] = col_info.length
545
+ info['precision'] = col_info.precision
546
+ info['scale'] = col_info.scale
547
+ info['column_definition'] = col_info.column_definition
548
+ # @Version 乐观锁字段:标记并强制 INTEGER 类型 + 默认 0
549
+ if isinstance(col_info, Version) or getattr(col_info, 'version', False):
550
+ info['version'] = True
551
+ info['nullable'] = False
552
+ if info['default'] is None:
553
+ info['default'] = 0
554
+ # 版本号统一用整型(与 JPA Version 语义一致)
555
+ if self.dialect == 'sqlite':
556
+ info['sql_type'] = 'INTEGER'
557
+ else:
558
+ info['sql_type'] = 'INTEGER'
559
+ return info
560
+ else:
561
+ info['name'] = _camel_to_snake(attr_name)
562
+
563
+ info['sql_type'] = _get_sql_type(py_type, self.dialect, info)
564
+ return info
565
+
566
+ def _quote(self, identifier: str) -> str:
567
+ """引用标识符(表名/列名)"""
568
+ if self.dialect == 'mysql':
569
+ return f"`{identifier}`"
570
+ return f'"{identifier}"'
571
+
572
+ def _build_create_table_sql(self, et: EntityTable) -> str:
573
+ """生成 CREATE TABLE 语句"""
574
+ cols_sql = []
575
+ primary_keys = []
576
+ unique_cols = []
577
+
578
+ for col in et.columns:
579
+ parts = [self._quote(col['name'])]
580
+ sql_type = col['sql_type']
581
+ is_pk = col['primary_key']
582
+ is_auto = col['auto_increment']
583
+
584
+ # SQLite: AUTOINCREMENT must be "INTEGER PRIMARY KEY AUTOINCREMENT" inline
585
+ if self.dialect == 'sqlite' and is_pk and is_auto:
586
+ parts = [self._quote(col['name']), "INTEGER", "PRIMARY KEY", "AUTOINCREMENT"]
587
+ primary_keys.append(col['name'])
588
+ else:
589
+ parts = [self._quote(col['name']), sql_type]
590
+ if is_pk:
591
+ if is_auto:
592
+ if self.dialect == 'mysql':
593
+ parts.append("AUTO_INCREMENT")
594
+ elif self.dialect == 'postgresql':
595
+ if 'BIGINT' in sql_type:
596
+ parts[1] = "BIGSERIAL"
597
+ elif 'INT' in sql_type:
598
+ parts[1] = "SERIAL"
599
+ parts.append("NOT NULL")
600
+ primary_keys.append(col['name'])
601
+ else:
602
+ if not col['nullable']:
603
+ parts.append("NOT NULL")
604
+ if col['unique']:
605
+ unique_cols.append(col['name'])
606
+ if col['default'] is not None:
607
+ if isinstance(col['default'], str):
608
+ parts.append(f"DEFAULT '{col['default']}'")
609
+ elif isinstance(col['default'], bool):
610
+ parts.append(f"DEFAULT {1 if col['default'] else 0}")
611
+ else:
612
+ parts.append(f"DEFAULT {col['default']}")
613
+ cols_sql.append(" ".join(parts))
614
+
615
+ # 对于非SQLite或复合主键,显式声明PRIMARY KEY
616
+ if primary_keys and not (self.dialect == 'sqlite' and len(primary_keys) == 1
617
+ and any(c['auto_increment'] for c in et.columns if c['name'] == primary_keys[0])):
618
+ cols_sql.append(f"PRIMARY KEY ({', '.join(self._quote(c) for c in primary_keys)})")
619
+ for uc in unique_cols:
620
+ cols_sql.append(f"UNIQUE ({self._quote(uc)})")
621
+
622
+ # 索引(MySQL不能在CREATE TABLE中声明INDEX,单独CREATE INDEX)
623
+ table_sql = f"CREATE TABLE {self._quote(et.table_name)} (\n "
624
+ table_sql += ",\n ".join(cols_sql)
625
+ table_sql += "\n)"
626
+
627
+ if self.dialect == 'mysql':
628
+ table_sql += " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
629
+ if et.comment:
630
+ table_sql += f" COMMENT='{et.comment}'"
631
+ elif self.dialect == 'postgresql' and et.comment:
632
+ table_sql += f"; COMMENT ON TABLE {self._quote(et.table_name)} IS '{et.comment}'"
633
+
634
+ return table_sql
635
+
636
+ def _get_existing_columns(self, table_name: str) -> Dict[str, dict]:
637
+ """查询现有表的列信息"""
638
+ conn = None
639
+ columns = {}
640
+ try:
641
+ pooled = self.pool.get_connection()
642
+ conn = pooled.connection
643
+ cursor = conn.cursor()
644
+ if self.dialect == 'mysql':
645
+ cursor.execute(f"SHOW COLUMNS FROM {self._quote(table_name)}")
646
+ for row in cursor.fetchall():
647
+ col_name = row[0] if not isinstance(row, dict) else row['Field']
648
+ col_type = row[1] if not isinstance(row, dict) else row['Type']
649
+ is_null = (row[2] == 'YES') if not isinstance(row, dict) else (row['Null'] == 'YES')
650
+ is_pk = False
651
+ key = row[3] if not isinstance(row, dict) else row.get('Key', '')
652
+ if key == 'PRI':
653
+ is_pk = True
654
+ columns[col_name] = {
655
+ 'name': col_name, 'type': col_type, 'nullable': is_null, 'primary_key': is_pk
656
+ }
657
+ elif self.dialect == 'postgresql':
658
+ cursor.execute("""
659
+ SELECT column_name, data_type, is_nullable,
660
+ (SELECT COUNT(*) FROM information_schema.table_constraints tc
661
+ JOIN information_schema.key_column_usage kcu
662
+ ON tc.constraint_name = kcu.constraint_name
663
+ WHERE tc.table_name = %s AND tc.constraint_type = 'PRIMARY KEY'
664
+ AND kcu.column_name = columns.column_name) > 0 as is_pk
665
+ FROM information_schema.columns
666
+ WHERE table_name = %s
667
+ """, (table_name, table_name))
668
+ for row in cursor.fetchall():
669
+ col_name, col_type, is_null, is_pk = row[0], row[1], row[2] == 'YES', row[3]
670
+ columns[col_name] = {'name': col_name, 'type': col_type, 'nullable': is_null, 'primary_key': is_pk}
671
+ elif self.dialect == 'sqlite':
672
+ cursor.execute(f"PRAGMA table_info({self._quote(table_name)})")
673
+ for row in cursor.fetchall():
674
+ col_name = row[1]
675
+ col_type = row[2]
676
+ # SQLite PRAGMA table_info: row[3] = notnull (1=NOT NULL, 0=nullable)
677
+ # row[5] = pk (1=primary key)
678
+ is_pk = bool(row[5])
679
+ notnull = bool(row[3]) or is_pk # 主键隐式为NOT NULL
680
+ is_null = not notnull
681
+ columns[col_name] = {'name': col_name, 'type': col_type, 'nullable': is_null, 'primary_key': is_pk}
682
+ cursor.close()
683
+ self.pool.return_connection(pooled)
684
+ except Exception as e:
685
+ logger.debug(f"Failed to get columns for {table_name}: {e}")
686
+ return columns
687
+
688
+ def _table_exists(self, table_name: str) -> bool:
689
+ """检查表是否存在"""
690
+ conn = None
691
+ try:
692
+ pooled = self.pool.get_connection()
693
+ conn = pooled.connection
694
+ cursor = conn.cursor()
695
+ if self.dialect == 'mysql':
696
+ cursor.execute("SHOW TABLES LIKE %s", (table_name,))
697
+ elif self.dialect == 'postgresql':
698
+ cursor.execute("SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = %s)", (table_name,))
699
+ else:
700
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
701
+ exists = cursor.fetchone() is not None
702
+ cursor.close()
703
+ self.pool.return_connection(pooled)
704
+ return exists
705
+ except Exception:
706
+ return False
707
+
708
+ def _get_existing_indexes(self, table_name: str) -> Dict[str, List[str]]:
709
+ """查询现有表的索引"""
710
+ indexes = {}
711
+ conn = None
712
+ try:
713
+ pooled = self.pool.get_connection()
714
+ conn = pooled.connection
715
+ cursor = conn.cursor()
716
+ if self.dialect == 'mysql':
717
+ cursor.execute(f"SHOW INDEX FROM {self._quote(table_name)}")
718
+ for row in cursor.fetchall():
719
+ idx_name = row[2] if not isinstance(row, dict) else row['Key_name']
720
+ col_name = row[4] if not isinstance(row, dict) else row['Column_name']
721
+ if idx_name == 'PRIMARY':
722
+ continue
723
+ if idx_name not in indexes:
724
+ indexes[idx_name] = []
725
+ indexes[idx_name].append(col_name)
726
+ elif self.dialect == 'sqlite':
727
+ cursor.execute(f"PRAGMA index_list({self._quote(table_name)})")
728
+ for row in cursor.fetchall():
729
+ idx_name = row[1]
730
+ if idx_name.startswith('sqlite_'):
731
+ continue
732
+ cursor2 = conn.cursor()
733
+ cursor2.execute(f"PRAGMA index_info({self._quote(idx_name)})")
734
+ cols = [r[2] for r in cursor2.fetchall()]
735
+ cursor2.close()
736
+ indexes[idx_name] = cols
737
+ cursor.close()
738
+ self.pool.return_connection(pooled)
739
+ except Exception as e:
740
+ logger.debug(f"Failed to get indexes for {table_name}: {e}")
741
+ return indexes
742
+
743
+ def _execute_sql(self, sql: str):
744
+ """执行单条SQL"""
745
+ conn = None
746
+ try:
747
+ pooled = self.pool.get_connection()
748
+ conn = pooled.connection
749
+ cursor = conn.cursor()
750
+ logger.debug(f"[DDL] Executing: {sql[:200]}")
751
+ cursor.execute(sql)
752
+ conn.commit()
753
+ cursor.close()
754
+ self.pool.return_connection(pooled)
755
+ self._executed_sql.append(sql)
756
+ except Exception as e:
757
+ if conn:
758
+ try:
759
+ conn.rollback()
760
+ except Exception:
761
+ pass
762
+ logger.error(f"[DDL] SQL execution failed: {e}\nSQL: {sql[:300]}")
763
+ raise
764
+
765
+ def execute(self) -> List[str]:
766
+ """根据 ddl-auto 模式执行 DDL 操作"""
767
+ if self.mode == DdlAutoMode.NONE:
768
+ logger.info("[DDL] ddl-auto=none, skipping DDL execution")
769
+ return []
770
+
771
+ self._parsed = [self._parse_entity(cls) for cls in self._entities]
772
+ if not self._parsed:
773
+ logger.info("[DDL] No entities registered, nothing to do")
774
+ return []
775
+
776
+ logger.info(f"[DDL] ddl-auto={self.mode.value}, processing {len(self._parsed)} table(s)")
777
+ self._executed_sql = []
778
+
779
+ try:
780
+ if self.mode == DdlAutoMode.CREATE:
781
+ self._execute_create()
782
+ elif self.mode == DdlAutoMode.CREATE_DROP:
783
+ self._execute_create()
784
+ # Note: drop will happen on shutdown if registered
785
+ elif self.mode == DdlAutoMode.UPDATE:
786
+ self._execute_update()
787
+ elif self.mode == DdlAutoMode.VALIDATE:
788
+ self._execute_validate()
789
+ except Exception as e:
790
+ logger.error(f"[DDL] Execution failed: {e}")
791
+ raise
792
+
793
+ logger.info(f"[DDL] Completed. Executed {len(self._executed_sql)} statement(s)")
794
+ return list(self._executed_sql)
795
+
796
+ def _execute_create(self):
797
+ """create 模式: DROP + CREATE"""
798
+ for et in self._parsed:
799
+ if self._table_exists(et.table_name):
800
+ drop_sql = f"DROP TABLE {self._quote(et.table_name)}"
801
+ if self.dialect == 'mysql':
802
+ drop_sql = f"DROP TABLE IF EXISTS {self._quote(et.table_name)}"
803
+ elif self.dialect == 'postgresql':
804
+ drop_sql = f"DROP TABLE IF EXISTS {self._quote(et.table_name)} CASCADE"
805
+ else:
806
+ drop_sql = f"DROP TABLE IF EXISTS {self._quote(et.table_name)}"
807
+ self._execute_sql(drop_sql)
808
+ logger.info(f"[DDL] Dropped table {et.table_name}")
809
+ create_sql = self._build_create_table_sql(et)
810
+ self._execute_sql(create_sql)
811
+ logger.info(f"[DDL] Created table {et.table_name} ({len(et.columns)} columns)")
812
+
813
+ def _execute_update(self):
814
+ """update 模式: 创建不存在的表,为已存在的表添加新列、创建缺失索引"""
815
+ for et in self._parsed:
816
+ if not self._table_exists(et.table_name):
817
+ create_sql = self._build_create_table_sql(et)
818
+ self._execute_sql(create_sql)
819
+ logger.info(f"[DDL] Created new table {et.table_name}")
820
+ # 创建表后创建索引
821
+ for idx in et.indexes:
822
+ self._create_index(et.table_name, idx)
823
+ continue
824
+ # 增量更新:添加新列
825
+ existing = self._get_existing_columns(et.table_name)
826
+ for col in et.columns:
827
+ if col['name'] not in existing:
828
+ parts = [f"ALTER TABLE {self._quote(et.table_name)} ADD COLUMN",
829
+ self._quote(col['name']), col['sql_type']]
830
+ if not col['nullable']:
831
+ parts.append("NOT NULL")
832
+ if col['default'] is not None:
833
+ if isinstance(col['default'], str):
834
+ parts.append(f"DEFAULT '{col['default']}'")
835
+ else:
836
+ parts.append(f"DEFAULT {col['default']}")
837
+ alter_sql = " ".join(parts)
838
+ self._execute_sql(alter_sql)
839
+ logger.info(f"[DDL] Added column {col['name']} to {et.table_name}")
840
+ # 创建缺失索引
841
+ existing_indexes = self._get_existing_indexes(et.table_name)
842
+ for idx in et.indexes:
843
+ if idx.name not in existing_indexes:
844
+ self._create_index(et.table_name, idx)
845
+
846
+ def _create_index(self, table_name: str, idx: Index):
847
+ """创建索引"""
848
+ unique = "UNIQUE" if idx.unique else ""
849
+ cols = ", ".join(self._quote(c) for c in idx.columns)
850
+ sql = f"CREATE {unique} INDEX {self._quote(idx.name)} ON {self._quote(table_name)} ({cols})"
851
+ self._execute_sql(sql)
852
+ logger.info(f"[DDL] Created index {idx.name} on {table_name}")
853
+
854
+ def _execute_validate(self):
855
+ """validate 模式: 验证表结构匹配"""
856
+ errors = []
857
+ for et in self._parsed:
858
+ if not self._table_exists(et.table_name):
859
+ errors.append(f"Table '{et.table_name}' does not exist")
860
+ continue
861
+ existing = self._get_existing_columns(et.table_name)
862
+ for col in et.columns:
863
+ if col['name'] not in existing:
864
+ errors.append(f"Column '{et.table_name}.{col['name']}' is missing")
865
+ else:
866
+ ex = existing[col['name']]
867
+ if col['primary_key'] != ex['primary_key']:
868
+ errors.append(f"Column '{et.table_name}.{col['name']}' primary key mismatch")
869
+ if not col['nullable'] and ex['nullable']:
870
+ errors.append(f"Column '{et.table_name}.{col['name']}' should be NOT NULL")
871
+ if errors:
872
+ error_msg = "Schema validation failed:\n " + "\n ".join(errors)
873
+ logger.error(f"[DDL] {error_msg}")
874
+ raise Exception(error_msg)
875
+ logger.info("[DDL] Schema validation passed")
876
+
877
+ def drop_all(self):
878
+ """create-drop 模式关闭时调用:删除所有注册实体对应的表"""
879
+ for et in reversed(self._parsed):
880
+ if self._table_exists(et.table_name):
881
+ drop_sql = f"DROP TABLE IF EXISTS {self._quote(et.table_name)}"
882
+ if self.dialect == 'postgresql':
883
+ drop_sql += " CASCADE"
884
+ self._execute_sql(drop_sql)
885
+ logger.info(f"[DDL] Dropped table {et.table_name}")
886
+
887
+ def get_generated_sql(self) -> List[str]:
888
+ """获取生成但不一定执行的SQL(用于预览)"""
889
+ # 如果还没解析,先解析实体
890
+ if not self._parsed:
891
+ self._parsed = [self._parse_entity(cls) for cls in self._entities]
892
+ result = []
893
+ for et in self._parsed:
894
+ result.append(self._build_create_table_sql(et))
895
+ return result
896
+
897
+ def get_executed_sql(self) -> List[str]:
898
+ """获取已执行的SQL"""
899
+ return list(self._executed_sql)
900
+
901
+
902
+ # ==================== 装饰器API(便捷使用) ====================
903
+
904
+ def entity(table_name: str = "", indexes: List[Index] = None, comment: str = ""):
905
+ """
906
+ @Entity 装饰器,标注一个类为JPA风格的实体类
907
+
908
+ Usage:
909
+ @entity("sys_user")
910
+ class User:
911
+ def __init__(self, id: int = None, username: str = "", email: str = ""):
912
+ self.id = id
913
+ self.username = username
914
+ self.email = email
915
+ """
916
+ t = Table(name=table_name, indexes=indexes, comment=comment)
917
+ def decorator(cls):
918
+ setattr(cls, '__entity__', True)
919
+ setattr(cls, '__table__', t)
920
+ if not table_name:
921
+ t.name = _camel_to_snake(cls.__name__)
922
+ return cls
923
+ return decorator
924
+
925
+
926
+ def table(name: str = "", indexes: List[Index] = None, comment: str = ""):
927
+ """@Table 装饰器,标注实体类对应的表(与@entity功能相同,别名)"""
928
+ return entity(name, indexes, comment)
929
+
930
+
931
+ # ==================== 全局集成 ====================
932
+
933
+ # 全局DDL自动管理器实例
934
+ _global_ddl_manager: Optional[DdlAutoManager] = None
935
+
936
+
937
+ def init_ddl_auto(connection_pool, config: dict = None) -> Optional[DdlAutoManager]:
938
+ """
939
+ 从配置初始化DDL自动建表管理器并执行
940
+
941
+ Args:
942
+ connection_pool: 数据库连接池实例
943
+ config: 配置字典,应包含 ddl-auto 配置段
944
+
945
+ Returns:
946
+ DdlAutoManager实例或None
947
+ """
948
+ global _global_ddl_manager
949
+ config = config or {}
950
+ ddl_config = config.get('ddl-auto', config.get('jpa', {}).get('hibernate', {}))
951
+ mode = str(ddl_config.get('mode', ddl_config.get('ddl-auto', 'none'))).lower()
952
+
953
+ if mode == 'none' or not mode:
954
+ logger.info("[DDL] ddl-auto=none, skipping auto DDL")
955
+ return None
956
+
957
+ # 判断方言
958
+ driver = str(config.get('driver', 'sqlite')).lower()
959
+ if 'mysql' in driver:
960
+ dialect = 'mysql'
961
+ elif 'postgresql' in driver or 'pg' in driver:
962
+ dialect = 'postgresql'
963
+ else:
964
+ dialect = 'sqlite'
965
+
966
+ # 创建管理器
967
+ manager = DdlAutoManager(connection_pool, dialect=dialect, mode=mode)
968
+
969
+ # 扫描实体包
970
+ entity_packages = ddl_config.get('entity_packages', ddl_config.get('packages-to-scan', []))
971
+ if isinstance(entity_packages, str):
972
+ entity_packages = [p.strip() for p in entity_packages.split(',') if p.strip()]
973
+
974
+ if entity_packages:
975
+ manager._scan_packages(entity_packages)
976
+ logger.info(f"[DDL] Scanned entity packages: {entity_packages}, found {len(manager._entities)} entities")
977
+
978
+ # 执行DDL
979
+ if manager._entities:
980
+ # 临时关闭DDL阻断
981
+ try:
982
+ if hasattr(connection_pool, 'config'):
983
+ connection_pool.config['security_block_ddl'] = False
984
+ except Exception:
985
+ pass
986
+ manager.execute()
987
+
988
+ _global_ddl_manager = manager
989
+ return manager
990
+
991
+
992
+ def get_ddl_manager() -> Optional[DdlAutoManager]:
993
+ """获取全局DDL管理器"""
994
+ return _global_ddl_manager
995
+
996
+
997
+ # ==================== JPA @Version 乐观锁执行器 ====================
998
+
999
+ class OptimisticLockError(Exception):
1000
+ """乐观锁冲突异常:UPDATE 影响行数为 0,说明版本号已变更或记录不存在。"""
1001
+
1002
+
1003
+ def _find_version_column(entity_class: Type) -> Optional[dict]:
1004
+ """解析实体类,返回 ``@Version`` 列元数据 dict(无则 None)。
1005
+
1006
+ 复用 ``DdlAutoManager._parse_entity`` 解析逻辑,避免重复实现字段扫描。
1007
+ """
1008
+ # 用一个临时 manager 仅做解析(不执行 DDL)
1009
+ tmp = DdlAutoManager.__new__(DdlAutoManager)
1010
+ tmp.dialect = 'sqlite'
1011
+ tmp.mode = DdlAutoMode.NONE
1012
+ try:
1013
+ et = tmp._parse_entity(entity_class)
1014
+ except Exception:
1015
+ return None
1016
+ for col in et.columns:
1017
+ if col.get('version'):
1018
+ return col
1019
+ return None
1020
+
1021
+
1022
+ class OptimisticLockExecutor:
1023
+ """``@Version`` 乐观锁更新执行器(对齐 JPA/Hibernate 乐观锁语义)。
1024
+
1025
+ 本框架内嵌 PyMyBatis **不自动**在 UPDATE 时注入 version 检查子句(与 JPA/Hibernate
1026
+ 的差异,已在 ``Version`` 注解文档标注)。本执行器提供等价的显式乐观锁更新:
1027
+ 生成 ``UPDATE table SET ... , version = version + 1 WHERE <pk> = ? AND version = ?``,
1028
+ 根据影响行数判断是否冲突。
1029
+
1030
+ 用法::
1031
+
1032
+ from spring.orm import OptimisticLockExecutor, Version, Id, entity
1033
+
1034
+ @entity("sys_user")
1035
+ class User:
1036
+ id = Id()
1037
+ version = Version()
1038
+ def __init__(self, id=None, name=None, version=0):
1039
+ self.id = id; self.name = name; self.version = version
1040
+
1041
+ executor = OptimisticLockExecutor(connection_pool, dialect="mysql")
1042
+ # 冲突时抛 OptimisticLockError
1043
+ executor.update(entity_class=User, entity=user_obj,
1044
+ set_fields={"name": "new_name"})
1045
+ # 或探测式(不抛错,返回是否成功)
1046
+ ok = executor.try_update(entity_class=User, entity=user_obj,
1047
+ set_fields={"name": "new_name"})
1048
+
1049
+ Args:
1050
+ connection_pool: 数据库连接池(需支持 ``connection()`` 上下文管理器)。
1051
+ dialect: SQL 方言(mysql/postgresql/sqlite),影响标识符引用。
1052
+ """
1053
+
1054
+ def __init__(self, connection_pool: Any, dialect: str = "mysql"):
1055
+ self.pool = connection_pool
1056
+ self.dialect = dialect.lower()
1057
+
1058
+ def _quote(self, identifier: str) -> str:
1059
+ if self.dialect == 'mysql':
1060
+ return f"`{identifier}`"
1061
+ return f'"{identifier}"'
1062
+
1063
+ def _find_pk(self, entity_class: Type) -> Optional[dict]:
1064
+ """返回主键列元数据。"""
1065
+ for col in self._parse_columns(entity_class):
1066
+ if col.get('primary_key'):
1067
+ return col
1068
+ return None
1069
+
1070
+ def _parse_columns(self, entity_class: Type) -> List[dict]:
1071
+ """解析实体类的全部列元数据(复用 ``DdlAutoManager._parse_entity``)。
1072
+
1073
+ 列元数据含 ``py_name``(Python 属性名)与 ``name``(SQL 列名),供
1074
+ ``set_fields`` 的属性名 -> 列名翻译使用,避免 ``Column(name=...)`` 自定义列名时
1075
+ 生成错误 SQL(与 JPA 实体元数据语义一致)。
1076
+ """
1077
+ tmp = DdlAutoManager.__new__(DdlAutoManager)
1078
+ tmp.dialect = self.dialect
1079
+ tmp.mode = DdlAutoMode.NONE
1080
+ try:
1081
+ et = tmp._parse_entity(entity_class)
1082
+ except Exception:
1083
+ return []
1084
+ return list(et.columns)
1085
+
1086
+ def _column_py_to_sql_map(self, entity_class: Type) -> Dict[str, str]:
1087
+ """构造 ``{py_name: sql_column_name}`` 映射,用于 ``set_fields`` 翻译。"""
1088
+ mapping: Dict[str, str] = {}
1089
+ for col in self._parse_columns(entity_class):
1090
+ py = col.get('py_name') or col.get('name')
1091
+ mapping[py] = col.get('name') or py
1092
+ return mapping
1093
+
1094
+ def update(
1095
+ self,
1096
+ entity_class: Type,
1097
+ entity: Any,
1098
+ set_fields: Dict[str, Any],
1099
+ ) -> int:
1100
+ """乐观锁更新:冲突时抛 ``OptimisticLockError``。
1101
+
1102
+ Args:
1103
+ entity_class: 实体类(带 ``@Version`` 与主键)。
1104
+ entity: 实体实例(提供主键值与当前 version)。
1105
+ set_fields: 要更新的字段 -> 值映射(不含 version,version 自动 +1)。
1106
+ Returns:
1107
+ 新版本号(旧 version + 1)。
1108
+ """
1109
+ version_col = _find_version_column(entity_class)
1110
+ if version_col is None:
1111
+ raise ValueError(
1112
+ f"{entity_class.__name__} 未声明 @Version 字段,无法乐观锁更新"
1113
+ )
1114
+ # 注意:必须在 try_update 之前捕获 old_version——try_update 成功后会回写
1115
+ # entity.version = old_version + 1,事后再读会得到已自增的值。
1116
+ old_version = getattr(entity, version_col['py_name'], 0) or 0
1117
+ ok = self.try_update(entity_class, entity, set_fields)
1118
+ if not ok:
1119
+ raise OptimisticLockError(
1120
+ f"乐观锁更新失败:{entity_class.__name__} 版本已变更或记录不存在"
1121
+ )
1122
+ return old_version + 1
1123
+
1124
+ def try_update(
1125
+ self,
1126
+ entity_class: Type,
1127
+ entity: Any,
1128
+ set_fields: Dict[str, Any],
1129
+ ) -> bool:
1130
+ """乐观锁更新(探测式):成功返回 True,冲突/记录不存在返回 False。"""
1131
+ version_col = _find_version_column(entity_class)
1132
+ if version_col is None:
1133
+ raise ValueError(f"{entity_class.__name__} 未声明 @Version 字段,无法乐观锁更新")
1134
+ pk_col = self._find_pk(entity_class)
1135
+ if pk_col is None:
1136
+ raise ValueError(f"{entity_class.__name__} 未找到主键字段")
1137
+
1138
+ table_name = self._resolve_table_name(entity_class)
1139
+ old_version = getattr(entity, version_col['py_name'], 0) or 0
1140
+ pk_value = getattr(entity, pk_col['py_name'], None)
1141
+ if pk_value is None:
1142
+ raise ValueError("实体主键值为空,无法乐观锁更新")
1143
+
1144
+ # 构造 UPDATE ... SET ..., version = version + 1 WHERE pk = ? AND version = ?
1145
+ # set_fields 的键为 Python 属性名,需按实体元数据翻译为真实 SQL 列名
1146
+ col_map = self._column_py_to_sql_map(entity_class)
1147
+ set_parts = [f"{self._quote(self._col_sql_name(f, col_map))} = ?" for f in set_fields]
1148
+ set_parts.append(f"{self._quote(version_col['name'])} = {self._quote(version_col['name'])} + 1")
1149
+ sql = (
1150
+ f"UPDATE {self._quote(table_name)} SET {', '.join(set_parts)} "
1151
+ f"WHERE {self._quote(pk_col['name'])} = ? AND {self._quote(version_col['name'])} = ?"
1152
+ )
1153
+ params = list(set_fields.values()) + [pk_value, old_version]
1154
+
1155
+ affected = self._execute_dml(sql, params)
1156
+ # 同步回写实体上的新版本号,便于后续操作
1157
+ if affected > 0:
1158
+ try:
1159
+ setattr(entity, version_col['py_name'], old_version + 1)
1160
+ except Exception:
1161
+ pass
1162
+ return affected > 0
1163
+
1164
+ def _col_sql_name(self, field_name: str, col_map: Dict[str, str]) -> str:
1165
+ """Python 属性名 -> SQL 列名。
1166
+
1167
+ 优先查实体元数据映射(``Column(name=...)`` 自定义列名);未命中时回退 snake_case,
1168
+ 兼容未声明 ``Column`` 的简单字段。
1169
+ """
1170
+ if field_name in col_map:
1171
+ return col_map[field_name]
1172
+ return _camel_to_snake(field_name)
1173
+
1174
+ def _resolve_table_name(self, entity_class: Type) -> str:
1175
+ table_meta = getattr(entity_class, '__table__', None)
1176
+ if isinstance(table_meta, Table) and table_meta.name:
1177
+ return table_meta.name
1178
+ tn = getattr(entity_class, '__tablename__', "")
1179
+ return tn or _camel_to_snake(entity_class.__name__)
1180
+
1181
+ def _execute_dml(self, sql: str, params: list) -> int:
1182
+ """执行 DML,返回影响行数。兼容 DBUtils 连接池与原生 connection。"""
1183
+ conn = None
1184
+ cursor = None
1185
+ try:
1186
+ if hasattr(self.pool, 'connection'):
1187
+ conn = self.pool.connection()
1188
+ else:
1189
+ conn = self.pool
1190
+ cursor = conn.cursor()
1191
+ affected = cursor.execute(sql, params)
1192
+ conn.commit()
1193
+ # 不同驱动返回语义不一:
1194
+ # - DBUtils/MySQLdb: execute 返回 rowcount (int)
1195
+ # - sqlite3: execute 返回 cursor 自身(非 int)
1196
+ # - psycopg2: execute 返回 None
1197
+ if not isinstance(affected, int):
1198
+ affected = getattr(cursor, 'rowcount', 0)
1199
+ return int(affected or 0)
1200
+ except Exception:
1201
+ try:
1202
+ if conn is not None:
1203
+ conn.rollback()
1204
+ except Exception:
1205
+ pass
1206
+ raise
1207
+ finally:
1208
+ if cursor is not None:
1209
+ try:
1210
+ cursor.close()
1211
+ except Exception:
1212
+ pass
1213
+ if conn is not None and hasattr(self.pool, 'connection'):
1214
+ try:
1215
+ conn.close()
1216
+ except Exception:
1217
+ pass