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,274 @@
|
|
|
1
|
+
"""Spring Data 仓库抽象(对齐 ``org.springframework.data.repository.PagingAndSortingRepository``)。
|
|
2
|
+
|
|
3
|
+
提供基于实体元数据的 CRUD + 分页 + 排序 + ``Specification`` 动态查询统一抽象。
|
|
4
|
+
**复用现有范式不重复造轮子**:
|
|
5
|
+
- 实体解析复用 ``DdlAutoManager._parse_entity``(与 ``OptimisticLockExecutor`` 同一 ``__new__`` 技巧),
|
|
6
|
+
无需 pool 即可解析表名/列/主键。
|
|
7
|
+
- SQL 执行复用 ``OptimisticLockExecutor`` 的轻量范式(pool + cursor + ``_quote``),
|
|
8
|
+
不强依赖 PyMyBatis ``SqlSession``,降低使用门槛。
|
|
9
|
+
- 列名翻译 ``_col_resolver`` 把 Python 属性名映射为真实列名(对齐 ``Column(name=...)``),
|
|
10
|
+
供 ``Sort`` / ``Specification`` 复用。
|
|
11
|
+
|
|
12
|
+
与 Java 差异:
|
|
13
|
+
- Java 的 Spring Data Repository 是接口 + 方法名解析(运行时动态代理生成实现);
|
|
14
|
+
Python 无等价元编程惯例,故采用**基类继承** + 显式方法,更符合 Python 习惯。
|
|
15
|
+
- ``@DataRepository`` 为标记注解(声明管理的实体类型),实际能力由继承基类获得。
|
|
16
|
+
"""
|
|
17
|
+
import inspect
|
|
18
|
+
from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar
|
|
19
|
+
|
|
20
|
+
from spring.data.page import Page, Pageable, Sort
|
|
21
|
+
from spring.data.specification import ColResolver, Specification
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_entity_table(entity_class: Type):
|
|
27
|
+
"""复用 ``DdlAutoManager._parse_entity`` 解析实体元数据(不依赖 pool)。
|
|
28
|
+
|
|
29
|
+
与 ``OptimisticLockExecutor._parse_columns`` 同一技巧:``__new__`` 绕过 ``__init__``
|
|
30
|
+
(后者需要 pool),直接调用纯解析方法。
|
|
31
|
+
"""
|
|
32
|
+
from spring.orm.ddl_auto import DdlAutoManager
|
|
33
|
+
tmp = DdlAutoManager.__new__(DdlAutoManager)
|
|
34
|
+
tmp.dialect = "sqlite"
|
|
35
|
+
tmp.mode = None # type: ignore
|
|
36
|
+
return tmp._parse_entity(entity_class)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PagingAndSortingRepository(Generic[T]):
|
|
40
|
+
"""分页排序仓库基类。
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
pool: 数据库连接池(需支持 ``connection()`` 返回带 ``cursor()``/``commit()``/``close()`` 的连接)。
|
|
44
|
+
entity_class: 管理的实体类(带 ``@entity``/``Column`` 元数据)。
|
|
45
|
+
dialect: SQL 方言(mysql/postgresql/sqlite),影响标识符引用。
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, pool: Any, entity_class: Type[T], dialect: str = "sqlite"):
|
|
49
|
+
self.pool = pool
|
|
50
|
+
self.entity_class = entity_class
|
|
51
|
+
self.dialect = dialect.lower()
|
|
52
|
+
self._table = _parse_entity_table(entity_class)
|
|
53
|
+
self._columns: List[dict] = [c for c in self._table.columns if not c.get("transient")]
|
|
54
|
+
self._pk = next((c for c in self._columns if c.get("primary_key")), None)
|
|
55
|
+
if self._pk is None:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"实体 {entity_class.__name__} 未声明主键(@Id),无法构建 Repository"
|
|
58
|
+
)
|
|
59
|
+
# py_name -> sql 列名 映射,供 Sort/Specification 翻译
|
|
60
|
+
self._col_map: Dict[str, str] = {
|
|
61
|
+
c.get("py_name") or c.get("name"): c.get("name") or c.get("py_name")
|
|
62
|
+
for c in self._columns
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ==================== 内部工具 ====================
|
|
66
|
+
|
|
67
|
+
def _quote(self, identifier: str) -> str:
|
|
68
|
+
if self.dialect == "mysql":
|
|
69
|
+
return f"`{identifier}`"
|
|
70
|
+
return f'"{identifier}"'
|
|
71
|
+
|
|
72
|
+
def _col_resolver(self, prop: str) -> str:
|
|
73
|
+
return self._col_map.get(prop, prop)
|
|
74
|
+
|
|
75
|
+
def _column_list(self) -> str:
|
|
76
|
+
return ", ".join(self._quote(c["name"]) for c in self._columns)
|
|
77
|
+
|
|
78
|
+
def _row_to_entity(self, row: tuple) -> T:
|
|
79
|
+
"""按列顺序把行元组转为实体实例。"""
|
|
80
|
+
obj = self.entity_class.__new__(self.entity_class)
|
|
81
|
+
for col, val in zip(self._columns, row):
|
|
82
|
+
setattr(obj, col["py_name"], val)
|
|
83
|
+
return obj # type: ignore
|
|
84
|
+
|
|
85
|
+
def _get_field(self, entity: T, py_name: str) -> Any:
|
|
86
|
+
return getattr(entity, py_name, None)
|
|
87
|
+
|
|
88
|
+
def _execute(self, sql: str, params: list, fetch: bool = False):
|
|
89
|
+
"""执行 SQL。fetch=True 返回行列表,否则返回影响行数。"""
|
|
90
|
+
conn = None
|
|
91
|
+
try:
|
|
92
|
+
if hasattr(self.pool, "connection"):
|
|
93
|
+
conn = self.pool.connection()
|
|
94
|
+
else:
|
|
95
|
+
conn = self.pool
|
|
96
|
+
cursor = conn.cursor()
|
|
97
|
+
cursor.execute(sql, params)
|
|
98
|
+
if fetch:
|
|
99
|
+
rows = cursor.fetchall()
|
|
100
|
+
conn.commit()
|
|
101
|
+
return rows
|
|
102
|
+
# DML:rowcount 为影响行数(sqlite3/MySQLdb/psycopg2 一致)
|
|
103
|
+
affected = cursor.rowcount
|
|
104
|
+
conn.commit()
|
|
105
|
+
return affected
|
|
106
|
+
finally:
|
|
107
|
+
if conn is not None and hasattr(self.pool, "connection"):
|
|
108
|
+
try:
|
|
109
|
+
conn.close()
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def _fetchone(self, sql: str, params: list):
|
|
114
|
+
conn = None
|
|
115
|
+
try:
|
|
116
|
+
if hasattr(self.pool, "connection"):
|
|
117
|
+
conn = self.pool.connection()
|
|
118
|
+
else:
|
|
119
|
+
conn = self.pool
|
|
120
|
+
cursor = conn.cursor()
|
|
121
|
+
cursor.execute(sql, params)
|
|
122
|
+
row = cursor.fetchone()
|
|
123
|
+
conn.commit()
|
|
124
|
+
return row
|
|
125
|
+
finally:
|
|
126
|
+
if conn is not None and hasattr(self.pool, "connection"):
|
|
127
|
+
try:
|
|
128
|
+
conn.close()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# ==================== CRUD ====================
|
|
133
|
+
|
|
134
|
+
def save(self, entity: T) -> T:
|
|
135
|
+
"""保存实体:主键已存在则 UPDATE,否则 INSERT。"""
|
|
136
|
+
pk_val = self._get_field(entity, self._pk["py_name"])
|
|
137
|
+
if pk_val is not None and self.exists_by_id(pk_val):
|
|
138
|
+
return self._update(entity)
|
|
139
|
+
return self._insert(entity)
|
|
140
|
+
|
|
141
|
+
def _insert(self, entity: T) -> T:
|
|
142
|
+
non_auto = [c for c in self._columns if not c.get("auto_increment")]
|
|
143
|
+
cols = ", ".join(self._quote(c["name"]) for c in non_auto)
|
|
144
|
+
placeholders = ", ".join("?" for _ in non_auto)
|
|
145
|
+
params = [self._get_field(entity, c["py_name"]) for c in non_auto]
|
|
146
|
+
sql = f"INSERT INTO {self._quote(self._table.table_name)} ({cols}) VALUES ({placeholders})"
|
|
147
|
+
self._execute(sql, params)
|
|
148
|
+
# 自增主键回填
|
|
149
|
+
if self._pk.get("auto_increment") and self._get_field(entity, self._pk["py_name"]) is None:
|
|
150
|
+
last = self._fetchone(
|
|
151
|
+
f"SELECT {self._quote(self._pk['name'])} FROM "
|
|
152
|
+
f"{self._quote(self._table.table_name)} ORDER BY "
|
|
153
|
+
f"{self._quote(self._pk['name'])} DESC LIMIT 1", []
|
|
154
|
+
)
|
|
155
|
+
if last:
|
|
156
|
+
setattr(entity, self._pk["py_name"], last[0])
|
|
157
|
+
return entity
|
|
158
|
+
|
|
159
|
+
def _update(self, entity: T) -> T:
|
|
160
|
+
non_pk = [c for c in self._columns if not c.get("primary_key")]
|
|
161
|
+
set_parts = ", ".join(f"{self._quote(c['name'])} = ?" for c in non_pk)
|
|
162
|
+
params = [self._get_field(entity, c["py_name"]) for c in non_pk]
|
|
163
|
+
pk_val = self._get_field(entity, self._pk["py_name"])
|
|
164
|
+
params.append(pk_val)
|
|
165
|
+
sql = (f"UPDATE {self._quote(self._table.table_name)} SET {set_parts} "
|
|
166
|
+
f"WHERE {self._quote(self._pk['name'])} = ?")
|
|
167
|
+
self._execute(sql, params)
|
|
168
|
+
return entity
|
|
169
|
+
|
|
170
|
+
def find_by_id(self, id_: Any) -> Optional[T]:
|
|
171
|
+
sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)} "
|
|
172
|
+
f"WHERE {self._quote(self._pk['name'])} = ?")
|
|
173
|
+
row = self._fetchone(sql, [id_])
|
|
174
|
+
return self._row_to_entity(row) if row else None
|
|
175
|
+
|
|
176
|
+
def exists_by_id(self, id_: Any) -> bool:
|
|
177
|
+
sql = (f"SELECT 1 FROM {self._quote(self._table.table_name)} "
|
|
178
|
+
f"WHERE {self._quote(self._pk['name'])} = ? LIMIT 1")
|
|
179
|
+
return self._fetchone(sql, [id_]) is not None
|
|
180
|
+
|
|
181
|
+
def count(self, specification: Optional[Specification] = None) -> int:
|
|
182
|
+
where, params = self._spec_where(specification)
|
|
183
|
+
sql = f"SELECT COUNT(*) FROM {self._quote(self._table.table_name)}{where}"
|
|
184
|
+
row = self._fetchone(sql, params)
|
|
185
|
+
return int(row[0]) if row else 0
|
|
186
|
+
|
|
187
|
+
def delete_by_id(self, id_: Any) -> int:
|
|
188
|
+
sql = (f"DELETE FROM {self._quote(self._table.table_name)} "
|
|
189
|
+
f"WHERE {self._quote(self._pk['name'])} = ?")
|
|
190
|
+
return self._execute(sql, [id_])
|
|
191
|
+
|
|
192
|
+
def delete(self, entity: T) -> int:
|
|
193
|
+
return self.delete_by_id(self._get_field(entity, self._pk["py_name"]))
|
|
194
|
+
|
|
195
|
+
def delete_all(self, specification: Optional[Specification] = None) -> int:
|
|
196
|
+
where, params = self._spec_where(specification)
|
|
197
|
+
sql = f"DELETE FROM {self._quote(self._table.table_name)}{where}"
|
|
198
|
+
return self._execute(sql, params)
|
|
199
|
+
|
|
200
|
+
# ==================== 查询(排序/分页/动态) ====================
|
|
201
|
+
|
|
202
|
+
def find_all(self,
|
|
203
|
+
sort: Optional[Sort] = None,
|
|
204
|
+
specification: Optional[Specification] = None,
|
|
205
|
+
pageable: Optional[Pageable] = None) -> Any:
|
|
206
|
+
"""统一查询入口。
|
|
207
|
+
|
|
208
|
+
- 仅传 ``sort``:返回 ``List[T]``。
|
|
209
|
+
- 仅传 ``specification``:返回 ``List[T]``。
|
|
210
|
+
- 传 ``pageable``:返回 ``Page[T]``(可同时带 ``specification``)。
|
|
211
|
+
"""
|
|
212
|
+
where, params = self._spec_where(specification)
|
|
213
|
+
if pageable is not None:
|
|
214
|
+
return self._find_page(pageable, where, params, specification)
|
|
215
|
+
order = self._sort_sql(sort or Sort.unsorted())
|
|
216
|
+
sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
|
|
217
|
+
f"{where}{order}")
|
|
218
|
+
rows = self._execute(sql, params, fetch=True)
|
|
219
|
+
return [self._row_to_entity(r) for r in rows]
|
|
220
|
+
|
|
221
|
+
def find_one(self, specification: Specification) -> Optional[T]:
|
|
222
|
+
where, params = self._spec_where(specification)
|
|
223
|
+
sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
|
|
224
|
+
f"{where} LIMIT 1")
|
|
225
|
+
row = self._fetchone(sql, params)
|
|
226
|
+
return self._row_to_entity(row) if row else None
|
|
227
|
+
|
|
228
|
+
def _find_page(self, pageable: Pageable, where: str, params: list,
|
|
229
|
+
specification: Optional[Specification] = None) -> Page:
|
|
230
|
+
order = self._sort_sql(pageable.sort)
|
|
231
|
+
# SQLite/MySQL/PostgreSQL 均支持 LIMIT/OFFSET
|
|
232
|
+
page_sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
|
|
233
|
+
f"{where}{order} LIMIT ? OFFSET ?")
|
|
234
|
+
rows = self._execute(page_sql, params + [pageable.limit, pageable.offset], fetch=True)
|
|
235
|
+
content = [self._row_to_entity(r) for r in rows]
|
|
236
|
+
# total 必须带同一 specification,否则分页总数与筛选条件不一致
|
|
237
|
+
total = self.count(specification)
|
|
238
|
+
return Page(content, pageable, total)
|
|
239
|
+
|
|
240
|
+
def _spec_where(self, specification: Optional[Specification]) -> tuple:
|
|
241
|
+
if specification is None:
|
|
242
|
+
return "", []
|
|
243
|
+
sql, params = specification.to_predicate(self._col_resolver)
|
|
244
|
+
return (f" WHERE {sql}" if sql else ""), params
|
|
245
|
+
|
|
246
|
+
def _sort_sql(self, sort: Sort) -> str:
|
|
247
|
+
if not sort.is_sorted:
|
|
248
|
+
return ""
|
|
249
|
+
return " ORDER BY " + sort.to_sql(lambda p: self._quote(self._col_resolver(p)))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ==================== 注解(标记) ====================
|
|
253
|
+
|
|
254
|
+
class DataRepository:
|
|
255
|
+
"""``@DataRepository(EntityClass)`` 标记注解:声明仓库管理的实体类型。
|
|
256
|
+
|
|
257
|
+
标记后可通过 ``get_data_repository_entity(cls)`` 读取实体类型,便于 IoC 集成
|
|
258
|
+
(如 ``@Bean`` 工厂方法根据实体类型构造 ``PagingAndSortingRepository``)。
|
|
259
|
+
|
|
260
|
+
注意:本注解仅声明元数据,**不**自动生成仓库实现——仓库能力由继承
|
|
261
|
+
``PagingAndSortingRepository`` 获得(对齐 Python 习惯,与 Java 接口代理方式不同)。
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(self, entity_class: Type):
|
|
265
|
+
self.entity_class = entity_class
|
|
266
|
+
|
|
267
|
+
def __call__(self, target: Type) -> Type:
|
|
268
|
+
setattr(target, "__data_repository__", self)
|
|
269
|
+
return target
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_data_repository_entity(cls: Type) -> Optional[Type]:
|
|
273
|
+
ann = getattr(cls, "__data_repository__", None)
|
|
274
|
+
return ann.entity_class if ann is not None else None
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Spring Data JPA 风格的动态查询 ``Specification``(对齐 ``org.springframework.data.jpa.domain.Specification``)。
|
|
2
|
+
|
|
3
|
+
将查询条件抽象为可组合的谓词,``to_predicate()`` 返回 ``(where_sql, params)``,
|
|
4
|
+
由 ``PagingAndSortingRepository`` 拼入 SQL。支持 ``and`` / ``or`` / ``not`` 复合。
|
|
5
|
+
|
|
6
|
+
与 Java 差异:
|
|
7
|
+
- Java 的 ``Specification.to_predicate(Root, CriteriaQuery, CriteriaBuilder)`` 依赖 JPA Criteria API;
|
|
8
|
+
Python 无等价物,故简化为返回 ``(where_sql_fragment, params)`` 元组。
|
|
9
|
+
- 列名翻译通过 ``col_resolver``(``property_name -> sql_column_name``)回调完成,
|
|
10
|
+
与 ``Pageable.Sort`` 一致,避免 ``Column(name=...)`` 自定义列名时生成错误 SQL。
|
|
11
|
+
"""
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, Callable, List, Optional, Tuple, TypeVar
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
# 谓词返回类型:(SQL 片段, 参数列表);SQL 片段为空串表示无条件
|
|
18
|
+
Predicate = Tuple[str, List[Any]]
|
|
19
|
+
ColResolver = Callable[[str], str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Specification(ABC):
|
|
23
|
+
"""查询规范抽象基类。"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
27
|
+
"""返回 ``(where_sql, params)``。``where_sql`` 为空串表示无条件。"""
|
|
28
|
+
|
|
29
|
+
def and_(self, other: "Specification") -> "Specification":
|
|
30
|
+
return And(self, other)
|
|
31
|
+
|
|
32
|
+
def or_(self, other: "Specification") -> "Specification":
|
|
33
|
+
return Or(self, other)
|
|
34
|
+
|
|
35
|
+
def not_(self) -> "Specification":
|
|
36
|
+
return Not(self)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve(prop: str, col_resolver: Optional[ColResolver]) -> str:
|
|
40
|
+
return col_resolver(prop) if col_resolver else prop
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _Empty(Specification):
|
|
44
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
45
|
+
return "", []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class And(Specification):
|
|
49
|
+
def __init__(self, *specs: Specification):
|
|
50
|
+
self.specs = specs
|
|
51
|
+
|
|
52
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
53
|
+
parts: List[str] = []
|
|
54
|
+
params: List[Any] = []
|
|
55
|
+
for s in self.specs:
|
|
56
|
+
sql, p = s.to_predicate(col_resolver)
|
|
57
|
+
if sql:
|
|
58
|
+
parts.append(f"({sql})")
|
|
59
|
+
params.extend(p)
|
|
60
|
+
if not parts:
|
|
61
|
+
return "", []
|
|
62
|
+
return " AND ".join(parts), params
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Or(Specification):
|
|
66
|
+
def __init__(self, *specs: Specification):
|
|
67
|
+
self.specs = specs
|
|
68
|
+
|
|
69
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
70
|
+
parts: List[str] = []
|
|
71
|
+
params: List[Any] = []
|
|
72
|
+
for s in self.specs:
|
|
73
|
+
sql, p = s.to_predicate(col_resolver)
|
|
74
|
+
if sql:
|
|
75
|
+
parts.append(f"({sql})")
|
|
76
|
+
params.extend(p)
|
|
77
|
+
if not parts:
|
|
78
|
+
return "", []
|
|
79
|
+
return " OR ".join(parts), params
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Not(Specification):
|
|
83
|
+
def __init__(self, spec: Specification):
|
|
84
|
+
self.spec = spec
|
|
85
|
+
|
|
86
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
87
|
+
sql, params = self.spec.to_predicate(col_resolver)
|
|
88
|
+
if not sql:
|
|
89
|
+
return "", []
|
|
90
|
+
return f"NOT ({sql})", params
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _Comparison(Specification):
|
|
94
|
+
"""单字段比较谓词基类。"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, property: str, op: str, value: Any, negated: bool = False):
|
|
97
|
+
self.property = property
|
|
98
|
+
self.op = op
|
|
99
|
+
self.value = value
|
|
100
|
+
self.negated = negated
|
|
101
|
+
|
|
102
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
103
|
+
col = _resolve(self.property, col_resolver)
|
|
104
|
+
op = self.op if not self.negated else _negate_op(self.op)
|
|
105
|
+
return f"{col} {op} ?", [self.value]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _negate_op(op: str) -> str:
|
|
109
|
+
return {
|
|
110
|
+
"=": "<>", "<>": "=", "!=": "=",
|
|
111
|
+
">": "<=", "<": ">=", ">=": "<", "<=": ">",
|
|
112
|
+
"LIKE": "NOT LIKE", "IN": "NOT IN", "IS": "IS NOT",
|
|
113
|
+
}.get(op, f"NOT {op}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _Like(Specification):
|
|
117
|
+
def __init__(self, property: str, pattern: str, case_insensitive: bool = False):
|
|
118
|
+
self.property = property
|
|
119
|
+
self.pattern = pattern
|
|
120
|
+
self.case_insensitive = case_insensitive
|
|
121
|
+
|
|
122
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
123
|
+
col = _resolve(self.property, col_resolver)
|
|
124
|
+
if self.case_insensitive:
|
|
125
|
+
return f"LOWER({col}) LIKE LOWER(?)", [self.pattern]
|
|
126
|
+
return f"{col} LIKE ?", [self.pattern]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _In(Specification):
|
|
130
|
+
def __init__(self, property: str, values: list):
|
|
131
|
+
self.property = property
|
|
132
|
+
self.values = list(values)
|
|
133
|
+
|
|
134
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
135
|
+
col = _resolve(self.property, col_resolver)
|
|
136
|
+
if not self.values:
|
|
137
|
+
return "1=0", [] # 空集合:恒假
|
|
138
|
+
placeholders = ", ".join("?" for _ in self.values)
|
|
139
|
+
return f"{col} IN ({placeholders})", list(self.values)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _IsNull(Specification):
|
|
143
|
+
def __init__(self, property: str, negate: bool = False):
|
|
144
|
+
self.property = property
|
|
145
|
+
self.negate = negate
|
|
146
|
+
|
|
147
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
148
|
+
col = _resolve(self.property, col_resolver)
|
|
149
|
+
return (f"{col} IS NOT NULL" if self.negate else f"{col} IS NULL"), []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _Between(Specification):
|
|
153
|
+
def __init__(self, property: str, low: Any, high: Any):
|
|
154
|
+
self.property = property
|
|
155
|
+
self.low = low
|
|
156
|
+
self.high = high
|
|
157
|
+
|
|
158
|
+
def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
|
|
159
|
+
col = _resolve(self.property, col_resolver)
|
|
160
|
+
return f"{col} BETWEEN ? AND ?", [self.low, self.high]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Specifications:
|
|
164
|
+
"""``Specification`` 静态工厂(对齐 Spring ``Specifications`` 工具类)。"""
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def empty() -> Specification:
|
|
168
|
+
return _Empty()
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def where(spec: Optional[Specification] = None) -> Specification:
|
|
172
|
+
return spec if spec is not None else _Empty()
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def equal(property: str, value: Any) -> Specification:
|
|
176
|
+
return _Comparison(property, "=", value)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def not_equal(property: str, value: Any) -> Specification:
|
|
180
|
+
return _Comparison(property, "<>", value)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def greater_than(property: str, value: Any) -> Specification:
|
|
184
|
+
return _Comparison(property, ">", value)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def greater_equal(property: str, value: Any) -> Specification:
|
|
188
|
+
return _Comparison(property, ">=", value)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def less_than(property: str, value: Any) -> Specification:
|
|
192
|
+
return _Comparison(property, "<", value)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def less_equal(property: str, value: Any) -> Specification:
|
|
196
|
+
return _Comparison(property, "<=", value)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def like(property: str, pattern: str, case_insensitive: bool = False) -> Specification:
|
|
200
|
+
return _Like(property, pattern, case_insensitive)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def in_(property: str, values: list) -> Specification:
|
|
204
|
+
return _In(property, values)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def is_null(property: str) -> Specification:
|
|
208
|
+
return _IsNull(property, negate=False)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def is_not_null(property: str) -> Specification:
|
|
212
|
+
return _IsNull(property, negate=True)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def between(property: str, low: Any, high: Any) -> Specification:
|
|
216
|
+
return _Between(property, low, high)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def and_(spec: Specification, *others: Specification) -> Specification:
|
|
220
|
+
return And(spec, *others)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def or_(spec: Specification, *others: Specification) -> Specification:
|
|
224
|
+
return Or(spec, *others)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def not_(spec: Specification) -> Specification:
|
|
228
|
+
return Not(spec)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""SpringBootAI 多数据源读写分离模块(对齐 Spring ``AbstractRoutingDataSource`` +
|
|
2
|
+
``dynamic-datasource-spring-boot-starter`` 的 ``@DS``/``@Master``/``@Slave``)。
|
|
3
|
+
|
|
4
|
+
模块组成:
|
|
5
|
+
- ``context``: ``DataSourceContextHolder`` —— ``ContextVar`` 持有当前路由键。
|
|
6
|
+
- ``dynamic``: ``DynamicRoutingDataSource`` —— 多池路由 + 从库轮询 + 故障回退。
|
|
7
|
+
- ``annotations``: ``@DS``/``@Master``/``@Slave`` 注解 + 方法级 AOP 切面。
|
|
8
|
+
|
|
9
|
+
典型用法::
|
|
10
|
+
|
|
11
|
+
from spring.datasource import (
|
|
12
|
+
DynamicRoutingDataSource, DS, Master, Slave,
|
|
13
|
+
DataSourceContextHolder, apply_ds_annotations,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# 1. 装配动态数据源(master + 两个 slave)
|
|
17
|
+
dynamic_ds = DynamicRoutingDataSource(
|
|
18
|
+
target_data_sources={"slave_1": pool1, "slave_2": pool2},
|
|
19
|
+
default_target_data_source=master_pool,
|
|
20
|
+
slave_keys=["slave_1", "slave_2"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# 2. 注解驱动路由
|
|
24
|
+
class OrderService:
|
|
25
|
+
@Master
|
|
26
|
+
def create_order(self, order): ... # 走主库写
|
|
27
|
+
@Slave
|
|
28
|
+
def list_orders(self): ... # 走从库读(轮询)
|
|
29
|
+
@DS("report_db")
|
|
30
|
+
def report(self): ... # 走具名数据源
|
|
31
|
+
|
|
32
|
+
设计原则:**复用项目既有范式**,注解继承 ``SpringAnnotation``,AOP 注册对齐
|
|
33
|
+
``@Validate``/``@Cacheable``,连接池接口对齐 ``ConnectionPool``,未引入第三方库。
|
|
34
|
+
|
|
35
|
+
与 Java 的差异:
|
|
36
|
+
- 用 ``ContextVar`` 替代 ``ThreadLocal``,兼容 ``asyncio`` 协程。
|
|
37
|
+
- ``@Slave`` 用占位路由键 + ``DynamicRoutingDataSource`` 轮询解析,无需运行时织入具体键。
|
|
38
|
+
"""
|
|
39
|
+
from .context import DataSourceContextHolder, routing_scope
|
|
40
|
+
from .dynamic import DynamicRoutingDataSource
|
|
41
|
+
from .annotations import (
|
|
42
|
+
DS, Master, Slave,
|
|
43
|
+
ds_route_decorator, ds_decorator_factory, apply_ds_annotations, is_slave_placeholder,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "1.0.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"DataSourceContextHolder",
|
|
50
|
+
"routing_scope",
|
|
51
|
+
"DynamicRoutingDataSource",
|
|
52
|
+
"DS", "Master", "Slave",
|
|
53
|
+
"ds_route_decorator", "ds_decorator_factory",
|
|
54
|
+
"apply_ds_annotations", "is_slave_placeholder",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# 接入 comprehensive_aop 受管 Bean 包装链路(对齐 @Validate/@Cacheable 注册模式)。
|
|
59
|
+
# 在受管 Bean 上标注 @DS/@Master/@Slave 时,IoC 容器会通过 apply_annotations 自动包装。
|
|
60
|
+
try:
|
|
61
|
+
from spring.aop.comprehensive_aop import ANNOTATION_DECORATORS
|
|
62
|
+
for _ann_cls in (DS, Master, Slave):
|
|
63
|
+
if _ann_cls not in ANNOTATION_DECORATORS:
|
|
64
|
+
ANNOTATION_DECORATORS[_ann_cls] = ds_decorator_factory
|
|
65
|
+
except ImportError: # pragma: no cover - comprehensive_aop 未安装时静默跳过
|
|
66
|
+
pass
|