aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""数据库管理器 - 命名多实例实现。
|
|
2
|
+
|
|
3
|
+
提供统一的数据库连接管理、会话创建和健康检查功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
|
|
12
|
+
from sqlalchemy import text
|
|
13
|
+
from sqlalchemy.exc import DisconnectionError, OperationalError
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
15
|
+
|
|
16
|
+
from aury.boot.common.logging import logger
|
|
17
|
+
from aury.boot.infrastructure.database.config import DatabaseConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatabaseManager:
|
|
21
|
+
"""数据库管理器(命名多实例)。
|
|
22
|
+
|
|
23
|
+
职责:
|
|
24
|
+
1. 管理数据库引擎和连接池
|
|
25
|
+
2. 提供会话工厂
|
|
26
|
+
3. 健康检查和重连机制
|
|
27
|
+
4. 生命周期管理
|
|
28
|
+
5. 支持多个命名实例,如主库/从库、不同业务数据库等
|
|
29
|
+
|
|
30
|
+
使用示例:
|
|
31
|
+
# 默认实例
|
|
32
|
+
db_manager = DatabaseManager.get_instance()
|
|
33
|
+
await db_manager.initialize()
|
|
34
|
+
|
|
35
|
+
# 命名实例
|
|
36
|
+
primary = DatabaseManager.get_instance("primary")
|
|
37
|
+
replica = DatabaseManager.get_instance("replica")
|
|
38
|
+
|
|
39
|
+
# 获取会话
|
|
40
|
+
async with db_manager.session() as session:
|
|
41
|
+
# 使用 session 进行数据库操作
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 清理
|
|
45
|
+
await db_manager.cleanup()
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_instances: dict[str, DatabaseManager] = {}
|
|
49
|
+
|
|
50
|
+
def __init__(self, name: str = "default") -> None:
|
|
51
|
+
"""初始化数据库管理器。
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: 实例名称
|
|
55
|
+
"""
|
|
56
|
+
self.name = name
|
|
57
|
+
self._initialized: bool = False
|
|
58
|
+
self._config: DatabaseConfig | None = None
|
|
59
|
+
self._engine: AsyncEngine | None = None
|
|
60
|
+
self._session_factory: async_sessionmaker | None = None
|
|
61
|
+
self._max_retries: int = 3
|
|
62
|
+
self._retry_delay: float = 1.0
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_instance(cls, name: str = "default") -> DatabaseManager:
|
|
66
|
+
"""获取指定名称的实例。
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
name: 实例名称,默认为 "default"
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
DatabaseManager: 数据库管理器实例
|
|
73
|
+
"""
|
|
74
|
+
if name not in cls._instances:
|
|
75
|
+
cls._instances[name] = cls(name)
|
|
76
|
+
return cls._instances[name]
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
80
|
+
"""重置实例(仅用于测试)。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
84
|
+
|
|
85
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
86
|
+
"""
|
|
87
|
+
if name is None:
|
|
88
|
+
cls._instances.clear()
|
|
89
|
+
elif name in cls._instances:
|
|
90
|
+
del cls._instances[name]
|
|
91
|
+
|
|
92
|
+
def configure(self, config: DatabaseConfig) -> None:
|
|
93
|
+
"""配置数据库管理器。
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
config: 数据库配置
|
|
97
|
+
"""
|
|
98
|
+
self._config = config
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def engine(self) -> AsyncEngine:
|
|
102
|
+
"""获取数据库引擎。"""
|
|
103
|
+
if self._engine is None:
|
|
104
|
+
raise RuntimeError("数据库管理器未初始化,请先调用 initialize()")
|
|
105
|
+
return self._engine
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def session_factory(self) -> async_sessionmaker:
|
|
109
|
+
"""获取会话工厂。"""
|
|
110
|
+
if self._session_factory is None:
|
|
111
|
+
raise RuntimeError("数据库管理器未初始化,请先调用 initialize()")
|
|
112
|
+
return self._session_factory
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_initialized(self) -> bool:
|
|
116
|
+
"""检查是否已初始化。"""
|
|
117
|
+
return self._initialized
|
|
118
|
+
|
|
119
|
+
async def initialize(
|
|
120
|
+
self,
|
|
121
|
+
url: str | None = None,
|
|
122
|
+
*,
|
|
123
|
+
echo: bool | None = None,
|
|
124
|
+
pool_size: int | None = None,
|
|
125
|
+
max_overflow: int | None = None,
|
|
126
|
+
pool_timeout: int | None = None,
|
|
127
|
+
pool_recycle: int | None = None,
|
|
128
|
+
isolation_level: str | None = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""初始化数据库连接。
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
url: 数据库连接字符串,默认从配置读取
|
|
134
|
+
echo: 是否打印SQL语句
|
|
135
|
+
pool_size: 连接池大小
|
|
136
|
+
max_overflow: 最大溢出连接数
|
|
137
|
+
pool_timeout: 连接超时时间(秒)
|
|
138
|
+
pool_recycle: 连接回收时间(秒)
|
|
139
|
+
isolation_level: 事务隔离级别
|
|
140
|
+
"""
|
|
141
|
+
if self._initialized:
|
|
142
|
+
logger.warning("数据库管理器已初始化,跳过重复初始化")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# 使用提供的参数或配置中的默认值
|
|
146
|
+
db_isolation_level: str | None = None
|
|
147
|
+
if self._config is not None:
|
|
148
|
+
database_url = url or self._config.url
|
|
149
|
+
db_echo = echo if echo is not None else self._config.echo
|
|
150
|
+
db_pool_size = pool_size or self._config.pool_size
|
|
151
|
+
db_max_overflow = max_overflow or self._config.max_overflow
|
|
152
|
+
db_pool_timeout = pool_timeout or self._config.pool_timeout
|
|
153
|
+
db_pool_recycle = pool_recycle or self._config.pool_recycle
|
|
154
|
+
db_isolation_level = isolation_level or self._config.isolation_level
|
|
155
|
+
else:
|
|
156
|
+
# 如果没有配置,使用环境变量
|
|
157
|
+
import os
|
|
158
|
+
database_url = url or os.getenv("DATABASE_URL")
|
|
159
|
+
if not database_url:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"数据库 URL 未配置。请通过以下方式之一提供:"
|
|
162
|
+
"1. 使用 DatabaseManager.configure() 设置配置"
|
|
163
|
+
"2. 通过 initialize(url=...) 参数传入"
|
|
164
|
+
"3. 设置环境变量 DATABASE_URL"
|
|
165
|
+
)
|
|
166
|
+
db_echo = echo if echo is not None else os.getenv("DB_ECHO", "false").lower() == "true"
|
|
167
|
+
db_pool_size = pool_size or int(os.getenv("DB_POOL_SIZE", "5"))
|
|
168
|
+
db_max_overflow = max_overflow or int(os.getenv("DB_MAX_OVERFLOW", "10"))
|
|
169
|
+
db_pool_timeout = pool_timeout or int(os.getenv("DB_POOL_TIMEOUT", "30"))
|
|
170
|
+
db_pool_recycle = pool_recycle or int(os.getenv("DB_POOL_RECYCLE", "1800"))
|
|
171
|
+
db_isolation_level = isolation_level or os.getenv("DATABASE_ISOLATION_LEVEL")
|
|
172
|
+
|
|
173
|
+
# 构建引擎参数
|
|
174
|
+
engine_kwargs: dict = {
|
|
175
|
+
"echo": db_echo,
|
|
176
|
+
"future": True,
|
|
177
|
+
"pool_pre_ping": True,
|
|
178
|
+
"pool_size": db_pool_size,
|
|
179
|
+
"max_overflow": db_max_overflow,
|
|
180
|
+
"pool_timeout": db_pool_timeout,
|
|
181
|
+
"pool_recycle": db_pool_recycle,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 添加隔离级别(如果配置了)
|
|
185
|
+
if db_isolation_level:
|
|
186
|
+
engine_kwargs["isolation_level"] = db_isolation_level
|
|
187
|
+
logger.info(f"事务隔离级别设置为: {db_isolation_level}")
|
|
188
|
+
|
|
189
|
+
self._engine = create_async_engine(database_url, **engine_kwargs)
|
|
190
|
+
|
|
191
|
+
self._session_factory = async_sessionmaker(
|
|
192
|
+
self._engine,
|
|
193
|
+
expire_on_commit=False,
|
|
194
|
+
autoflush=False,
|
|
195
|
+
class_=AsyncSession,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# 验证连接
|
|
199
|
+
await self.health_check()
|
|
200
|
+
|
|
201
|
+
self._initialized = True
|
|
202
|
+
logger.info("数据库管理器初始化完成")
|
|
203
|
+
|
|
204
|
+
async def health_check(self) -> bool:
|
|
205
|
+
"""健康检查。
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
bool: 连接是否正常
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
async with self.engine.begin() as conn:
|
|
212
|
+
await conn.execute(text("SELECT 1"))
|
|
213
|
+
logger.debug("数据库健康检查通过")
|
|
214
|
+
return True
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
logger.error(f"数据库健康检查失败: {exc}")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
async def _check_session_connection(self, session: AsyncSession) -> None:
|
|
220
|
+
"""检查会话连接状态,必要时重连。
|
|
221
|
+
|
|
222
|
+
使用 SQLAlchemy 的通用异常类,支持所有数据库后端。
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
session: 数据库会话
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
Exception: 重试次数耗尽后抛出异常
|
|
229
|
+
"""
|
|
230
|
+
retries = self._max_retries
|
|
231
|
+
while retries > 0:
|
|
232
|
+
try:
|
|
233
|
+
await session.execute(text("SELECT 1"))
|
|
234
|
+
return
|
|
235
|
+
except (DisconnectionError, OperationalError) as exc:
|
|
236
|
+
logger.warning(f"数据库连接丢失,剩余重试次数: {retries}, 错误: {exc}")
|
|
237
|
+
retries -= 1
|
|
238
|
+
if retries == 0:
|
|
239
|
+
logger.error("数据库连接重试失败")
|
|
240
|
+
raise
|
|
241
|
+
await asyncio.sleep(self._retry_delay)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
# 其他异常直接抛出,不重试
|
|
244
|
+
logger.error(f"数据库连接检查失败: {exc}")
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
@asynccontextmanager
|
|
248
|
+
async def session(self) -> AsyncGenerator[AsyncSession]:
|
|
249
|
+
"""获取数据库会话(上下文管理器)。
|
|
250
|
+
|
|
251
|
+
Yields:
|
|
252
|
+
AsyncSession: 数据库会话
|
|
253
|
+
|
|
254
|
+
使用示例:
|
|
255
|
+
async with db_manager.session() as session:
|
|
256
|
+
result = await session.execute(query)
|
|
257
|
+
"""
|
|
258
|
+
session = self.session_factory()
|
|
259
|
+
try:
|
|
260
|
+
await self._check_session_connection(session)
|
|
261
|
+
yield session
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
await session.rollback()
|
|
264
|
+
logger.exception(f"数据库会话异常: {exc}")
|
|
265
|
+
raise
|
|
266
|
+
finally:
|
|
267
|
+
await session.close()
|
|
268
|
+
|
|
269
|
+
async def create_session(self) -> AsyncSession:
|
|
270
|
+
"""创建新的数据库会话(需要手动关闭)。
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
AsyncSession: 数据库会话
|
|
274
|
+
|
|
275
|
+
注意:使用后需要手动调用 await session.close()
|
|
276
|
+
建议使用 session() 上下文管理器代替此方法。
|
|
277
|
+
"""
|
|
278
|
+
session = self.session_factory()
|
|
279
|
+
await self._check_session_connection(session)
|
|
280
|
+
return session
|
|
281
|
+
|
|
282
|
+
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
|
|
283
|
+
"""FastAPI 依赖注入专用的会话获取器。
|
|
284
|
+
|
|
285
|
+
Yields:
|
|
286
|
+
AsyncSession: 数据库会话
|
|
287
|
+
|
|
288
|
+
使用示例(FastAPI 路由):
|
|
289
|
+
@router.get("/items")
|
|
290
|
+
async def get_items(
|
|
291
|
+
session: AsyncSession = Depends(db_manager.get_session),
|
|
292
|
+
):
|
|
293
|
+
...
|
|
294
|
+
"""
|
|
295
|
+
async with self.session() as session:
|
|
296
|
+
yield session
|
|
297
|
+
|
|
298
|
+
async def cleanup(self) -> None:
|
|
299
|
+
"""清理资源,关闭所有连接。"""
|
|
300
|
+
if self._engine is not None:
|
|
301
|
+
await self._engine.dispose()
|
|
302
|
+
logger.info("数据库连接已关闭")
|
|
303
|
+
|
|
304
|
+
self._engine = None
|
|
305
|
+
self._session_factory = None
|
|
306
|
+
self._initialized = False
|
|
307
|
+
|
|
308
|
+
def __repr__(self) -> str:
|
|
309
|
+
"""字符串表示。"""
|
|
310
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
311
|
+
return f"<DatabaseManager status={status}>"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
__all__ = [
|
|
315
|
+
"DatabaseManager",
|
|
316
|
+
]
|
|
317
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""查询优化工具。
|
|
2
|
+
|
|
3
|
+
提供缓存和性能监控装饰器。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from functools import wraps
|
|
10
|
+
import hashlib
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from aury.boot.common.logging import logger
|
|
14
|
+
|
|
15
|
+
# 查询性能监控配置
|
|
16
|
+
QUERY_SLOW_THRESHOLD = 1.0 # 慢查询阈值(秒)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cache_query(
|
|
20
|
+
ttl: int = 300,
|
|
21
|
+
key_prefix: str = "",
|
|
22
|
+
key_func: Callable | None = None,
|
|
23
|
+
) -> Callable:
|
|
24
|
+
"""查询结果缓存装饰器。
|
|
25
|
+
|
|
26
|
+
缓存查询结果,减少数据库访问。
|
|
27
|
+
集成现有的 CacheManager。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
ttl: 缓存过期时间(秒),默认 300 秒
|
|
31
|
+
key_prefix: 缓存键前缀
|
|
32
|
+
key_func: 自定义缓存键生成函数
|
|
33
|
+
|
|
34
|
+
用法:
|
|
35
|
+
class UserRepository(BaseRepository):
|
|
36
|
+
@cache_query(ttl=600, key_prefix="user")
|
|
37
|
+
async def get_by_email(self, email: str):
|
|
38
|
+
return await self.get_by(email=email)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def decorator(func: Callable) -> Callable:
|
|
42
|
+
@wraps(func)
|
|
43
|
+
async def wrapper(self, *args, **kwargs):
|
|
44
|
+
try:
|
|
45
|
+
from aury.boot.infrastructure.cache import CacheManager
|
|
46
|
+
|
|
47
|
+
cache = CacheManager.get_instance()
|
|
48
|
+
|
|
49
|
+
# 生成缓存键
|
|
50
|
+
if key_func:
|
|
51
|
+
cache_key = key_func(self, *args, **kwargs)
|
|
52
|
+
else:
|
|
53
|
+
# 默认缓存键生成策略
|
|
54
|
+
key_parts = [key_prefix, func.__name__]
|
|
55
|
+
if args:
|
|
56
|
+
key_parts.extend(str(arg) for arg in args)
|
|
57
|
+
if kwargs:
|
|
58
|
+
key_parts.extend(f"{k}:{v}" for k, v in sorted(kwargs.items()))
|
|
59
|
+
|
|
60
|
+
# 使用 MD5 生成固定长度的键
|
|
61
|
+
key_str = ":".join(str(p) for p in key_parts)
|
|
62
|
+
cache_key = f"repo:{hashlib.md5(key_str.encode()).hexdigest()}"
|
|
63
|
+
|
|
64
|
+
# 尝试从缓存获取
|
|
65
|
+
cached_result = await cache.get(cache_key)
|
|
66
|
+
if cached_result is not None:
|
|
67
|
+
logger.debug(f"缓存命中: {cache_key}")
|
|
68
|
+
return cached_result
|
|
69
|
+
|
|
70
|
+
# 执行查询
|
|
71
|
+
result = await func(self, *args, **kwargs)
|
|
72
|
+
|
|
73
|
+
# 存入缓存
|
|
74
|
+
await cache.set(cache_key, result, expire=ttl)
|
|
75
|
+
logger.debug(f"缓存写入: {cache_key}, TTL={ttl}s")
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
except Exception as e:
|
|
79
|
+
# 缓存失败不影响主流程
|
|
80
|
+
logger.warning(f"查询缓存失败: {e},继续执行查询")
|
|
81
|
+
return await func(self, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
return wrapper
|
|
84
|
+
|
|
85
|
+
return decorator
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def monitor_query(
|
|
89
|
+
slow_threshold: float = QUERY_SLOW_THRESHOLD,
|
|
90
|
+
enable_explain: bool = False,
|
|
91
|
+
) -> Callable:
|
|
92
|
+
"""查询性能监控装饰器。
|
|
93
|
+
|
|
94
|
+
监控查询执行时间,记录慢查询日志。
|
|
95
|
+
支持 SQLAlchemy explain() 功能。
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
slow_threshold: 慢查询阈值(秒),默认 1.0 秒
|
|
99
|
+
enable_explain: 是否启用查询计划分析
|
|
100
|
+
|
|
101
|
+
用法:
|
|
102
|
+
class UserRepository(BaseRepository):
|
|
103
|
+
@monitor_query(slow_threshold=0.5)
|
|
104
|
+
async def list(self, **filters):
|
|
105
|
+
return await super().list(**filters)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def decorator(func: Callable) -> Callable:
|
|
109
|
+
@wraps(func)
|
|
110
|
+
async def wrapper(self, *args, **kwargs):
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# 执行查询
|
|
115
|
+
result = await func(self, *args, **kwargs)
|
|
116
|
+
|
|
117
|
+
# 计算执行时间
|
|
118
|
+
duration = time.time() - start_time
|
|
119
|
+
|
|
120
|
+
# 记录慢查询
|
|
121
|
+
if duration >= slow_threshold:
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"慢查询检测: {func.__name__} 执行时间 {duration:.3f}s "
|
|
124
|
+
f"(阈值: {slow_threshold}s)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 如果启用 explain,尝试获取查询计划
|
|
128
|
+
if enable_explain and hasattr(self, "_last_query"):
|
|
129
|
+
try:
|
|
130
|
+
from sqlalchemy import text
|
|
131
|
+
explain_result = await self._session.execute(
|
|
132
|
+
text(f"EXPLAIN {self._last_query!s}")
|
|
133
|
+
)
|
|
134
|
+
explain_text = "\n".join(str(row) for row in explain_result)
|
|
135
|
+
logger.debug(f"查询计划:\n{explain_text}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.debug(f"无法获取查询计划: {e}")
|
|
138
|
+
|
|
139
|
+
# 记录正常查询(调试级别)
|
|
140
|
+
else:
|
|
141
|
+
logger.debug(
|
|
142
|
+
f"查询执行: {func.__name__} 耗时 {duration:.3f}s"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
except Exception as e:
|
|
147
|
+
duration = time.time() - start_time
|
|
148
|
+
logger.error(
|
|
149
|
+
f"查询失败: {func.__name__} 执行时间 {duration:.3f}s, 错误: {e}"
|
|
150
|
+
)
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
return wrapper
|
|
154
|
+
|
|
155
|
+
return decorator
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"cache_query",
|
|
160
|
+
"monitor_query",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""UPSERT 策略实现。
|
|
2
|
+
|
|
3
|
+
为不同数据库提供 UPSERT 操作的策略实现。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import Table, text
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
|
+
|
|
14
|
+
from aury.boot.common.logging import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UpsertStrategy(ABC):
|
|
18
|
+
"""UPSERT 策略抽象基类。
|
|
19
|
+
|
|
20
|
+
为不同数据库提供 UPSERT 操作的统一接口。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def execute(
|
|
25
|
+
self,
|
|
26
|
+
session: AsyncSession,
|
|
27
|
+
table: Table,
|
|
28
|
+
data_list: list[dict[str, Any]],
|
|
29
|
+
index_elements: list[str],
|
|
30
|
+
update_columns: list[str],
|
|
31
|
+
) -> None:
|
|
32
|
+
"""执行 UPSERT 操作。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
session: 数据库会话
|
|
36
|
+
table: 表对象
|
|
37
|
+
data_list: 数据字典列表
|
|
38
|
+
index_elements: 索引字段列表
|
|
39
|
+
update_columns: 需要更新的字段列表
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PostgreSQLUpsertStrategy(UpsertStrategy):
|
|
45
|
+
"""PostgreSQL UPSERT 策略。
|
|
46
|
+
|
|
47
|
+
使用 ON CONFLICT DO UPDATE 语法。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
async def execute(
|
|
51
|
+
self,
|
|
52
|
+
session: AsyncSession,
|
|
53
|
+
table: Table,
|
|
54
|
+
data_list: list[dict[str, Any]],
|
|
55
|
+
index_elements: list[str],
|
|
56
|
+
update_columns: list[str],
|
|
57
|
+
) -> None:
|
|
58
|
+
"""执行 PostgreSQL UPSERT。"""
|
|
59
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
60
|
+
|
|
61
|
+
stmt = pg_insert(table).values(data_list)
|
|
62
|
+
stmt = stmt.on_conflict_do_update(
|
|
63
|
+
index_elements=index_elements,
|
|
64
|
+
set_={col: stmt.excluded[col] for col in update_columns}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await session.execute(stmt)
|
|
68
|
+
await session.flush()
|
|
69
|
+
logger.debug(f"批量插入或更新 {len(data_list)} 条记录(PostgreSQL)")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SQLiteUpsertStrategy(UpsertStrategy):
|
|
73
|
+
"""SQLite UPSERT 策略。
|
|
74
|
+
|
|
75
|
+
使用 ON CONFLICT DO UPDATE 语法(SQLite 3.24.0+)。
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def execute(
|
|
79
|
+
self,
|
|
80
|
+
session: AsyncSession,
|
|
81
|
+
table: Table,
|
|
82
|
+
data_list: list[dict[str, Any]],
|
|
83
|
+
index_elements: list[str],
|
|
84
|
+
update_columns: list[str],
|
|
85
|
+
) -> None:
|
|
86
|
+
"""执行 SQLite UPSERT。"""
|
|
87
|
+
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
88
|
+
|
|
89
|
+
stmt = sqlite_insert(table).values(data_list)
|
|
90
|
+
stmt = stmt.on_conflict_do_update(
|
|
91
|
+
index_elements=index_elements,
|
|
92
|
+
set_={col: stmt.excluded[col] for col in update_columns}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
await session.execute(stmt)
|
|
96
|
+
await session.flush()
|
|
97
|
+
logger.debug(f"批量插入或更新 {len(data_list)} 条记录(SQLite)")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MySQLUpsertStrategy(UpsertStrategy):
|
|
101
|
+
"""MySQL UPSERT 策略。
|
|
102
|
+
|
|
103
|
+
使用 ON DUPLICATE KEY UPDATE 语法。
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
async def execute(
|
|
107
|
+
self,
|
|
108
|
+
session: AsyncSession,
|
|
109
|
+
table: Table,
|
|
110
|
+
data_list: list[dict[str, Any]],
|
|
111
|
+
index_elements: list[str],
|
|
112
|
+
update_columns: list[str],
|
|
113
|
+
) -> None:
|
|
114
|
+
"""执行 MySQL UPSERT。"""
|
|
115
|
+
# 构建批量 INSERT ... ON DUPLICATE KEY UPDATE 语句
|
|
116
|
+
columns = list(data_list[0].keys())
|
|
117
|
+
column_names = ", ".join(columns)
|
|
118
|
+
|
|
119
|
+
# 构建 VALUES 子句(批量插入)
|
|
120
|
+
values_placeholders = []
|
|
121
|
+
params = {}
|
|
122
|
+
for idx, data in enumerate(data_list):
|
|
123
|
+
row_placeholders = []
|
|
124
|
+
for col in columns:
|
|
125
|
+
param_name = f"{col}_{idx}"
|
|
126
|
+
row_placeholders.append(f":{param_name}")
|
|
127
|
+
params[param_name] = data[col]
|
|
128
|
+
values_placeholders.append(f"({', '.join(row_placeholders)})")
|
|
129
|
+
|
|
130
|
+
values_clause = ", ".join(values_placeholders)
|
|
131
|
+
|
|
132
|
+
# 构建 ON DUPLICATE KEY UPDATE 子句
|
|
133
|
+
update_clauses = [f"{col} = VALUES({col})" for col in update_columns]
|
|
134
|
+
update_clause = ", ".join(update_clauses)
|
|
135
|
+
|
|
136
|
+
# 执行 SQL
|
|
137
|
+
sql = f"INSERT INTO {table.name} ({column_names}) VALUES {values_clause} ON DUPLICATE KEY UPDATE {update_clause}"
|
|
138
|
+
await session.execute(text(sql), params)
|
|
139
|
+
await session.flush()
|
|
140
|
+
logger.debug(f"批量插入或更新 {len(data_list)} 条记录(MySQL)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class UpsertStrategyFactory:
|
|
144
|
+
"""UPSERT 策略工厂。
|
|
145
|
+
|
|
146
|
+
根据数据库方言创建相应的策略实例。
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
_strategies: ClassVar[dict[str, type[UpsertStrategy]]] = {
|
|
150
|
+
"postgresql": PostgreSQLUpsertStrategy,
|
|
151
|
+
"sqlite": SQLiteUpsertStrategy,
|
|
152
|
+
"mysql": MySQLUpsertStrategy,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def create(cls, dialect_name: str) -> UpsertStrategy:
|
|
157
|
+
"""创建 UPSERT 策略实例。
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
dialect_name: 数据库方言名称
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
UpsertStrategy: 策略实例
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
NotImplementedError: 如果数据库不支持 UPSERT 操作
|
|
167
|
+
"""
|
|
168
|
+
strategy_class = cls._strategies.get(dialect_name)
|
|
169
|
+
if strategy_class is None:
|
|
170
|
+
raise NotImplementedError(
|
|
171
|
+
f"数据库 {dialect_name} 不支持 UPSERT 操作。"
|
|
172
|
+
"请使用 bulk_insert() 或实现自定义的插入/更新逻辑。"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return strategy_class()
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def register(cls, dialect_name: str, strategy_class: type[UpsertStrategy]) -> None:
|
|
179
|
+
"""注册自定义策略。
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
dialect_name: 数据库方言名称
|
|
183
|
+
strategy_class: 策略类
|
|
184
|
+
"""
|
|
185
|
+
cls._strategies[dialect_name] = strategy_class
|
|
186
|
+
logger.debug(f"注册 UPSERT 策略: {dialect_name} -> {strategy_class.__name__}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
__all__ = [
|
|
190
|
+
"MySQLUpsertStrategy",
|
|
191
|
+
"PostgreSQLUpsertStrategy",
|
|
192
|
+
"SQLiteUpsertStrategy",
|
|
193
|
+
"UpsertStrategy",
|
|
194
|
+
"UpsertStrategyFactory",
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|