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,423 @@
|
|
|
1
|
+
"""Domain 层仓储实现 - BaseRepository 的具体实现。
|
|
2
|
+
|
|
3
|
+
提供通用的 CRUD 操作实现,是 IRepository 接口的具体实现。
|
|
4
|
+
|
|
5
|
+
**重要架构决策**:本模块在 domain 层实现了 IRepository 接口。
|
|
6
|
+
Infrastructure 层现在仅负责数据库连接管理,完全不依赖 domain 层。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Union, cast
|
|
13
|
+
|
|
14
|
+
from sqlalchemy import Select, delete, func, select
|
|
15
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
16
|
+
|
|
17
|
+
from aury.boot.common.logging import logger
|
|
18
|
+
from aury.boot.domain.exceptions import VersionConflictError
|
|
19
|
+
from aury.boot.domain.models import GUID, Base
|
|
20
|
+
from aury.boot.domain.transaction import _transaction_depth
|
|
21
|
+
from aury.boot.domain.pagination import (
|
|
22
|
+
PaginationParams,
|
|
23
|
+
PaginationResult,
|
|
24
|
+
SortParams,
|
|
25
|
+
)
|
|
26
|
+
from aury.boot.domain.repository.interface import IRepository
|
|
27
|
+
from aury.boot.domain.repository.query_builder import QueryBuilder
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from typing import Self
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_model_attr(model_class: type, attr_name: str) -> Any:
|
|
34
|
+
"""安全地获取模型类的属性,避免类型检查器警告。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
model_class: SQLAlchemy 模型类
|
|
38
|
+
attr_name: 属性名称
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
属性值(如果存在),否则返回 None
|
|
42
|
+
"""
|
|
43
|
+
return cast(Any, getattr(model_class, attr_name, None))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BaseRepository[ModelType: Base](IRepository[ModelType]):
|
|
47
|
+
"""仓储基类实现。提供通用 CRUD 操作。
|
|
48
|
+
|
|
49
|
+
**重要**:使用此类的模型必须至少继承以下 Mixin 之一:
|
|
50
|
+
- IDMixin: 标准自增整数主键
|
|
51
|
+
- UUIDMixin: UUID 主键
|
|
52
|
+
|
|
53
|
+
可选 Mixin:
|
|
54
|
+
- AuditableStateMixin: 软删除支持
|
|
55
|
+
- VersionMixin: 乐观锁支持
|
|
56
|
+
- TimestampMixin: 时间戳字段
|
|
57
|
+
|
|
58
|
+
自动提交行为:
|
|
59
|
+
- auto_commit=True(默认):非事务中自动 commit
|
|
60
|
+
- auto_commit=False:只 flush,需要手动管理事务或使用 .with_commit()
|
|
61
|
+
- 在事务中(transactional_context 等):永远不自动 commit,由事务统一管理
|
|
62
|
+
|
|
63
|
+
示例:
|
|
64
|
+
class User(IDMixin, TimestampMixin, AuditableStateMixin, Base):
|
|
65
|
+
__tablename__ = "users"
|
|
66
|
+
name: Mapped[str]
|
|
67
|
+
|
|
68
|
+
# 默认 auto_commit=True,非事务中自动提交
|
|
69
|
+
repo = UserRepository(session, User)
|
|
70
|
+
await repo.create({"name": "test"}) # 自动 commit
|
|
71
|
+
|
|
72
|
+
# auto_commit=False,需要手动管理
|
|
73
|
+
repo = UserRepository(session, User, auto_commit=False)
|
|
74
|
+
await repo.create({"name": "test"}) # 只 flush
|
|
75
|
+
await repo.with_commit().create({"name": "test2"}) # 强制 commit
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
session: AsyncSession,
|
|
81
|
+
model_class: type[ModelType],
|
|
82
|
+
auto_commit: bool = True,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._session = session
|
|
85
|
+
self._model_class = model_class
|
|
86
|
+
self._auto_commit = auto_commit
|
|
87
|
+
self._force_commit = False # 单次强制提交标记(用于 with_commit)
|
|
88
|
+
logger.debug(f"初始化 {self.__class__.__name__}")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def session(self) -> AsyncSession:
|
|
92
|
+
return self._session
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def model_class(self) -> type[ModelType]:
|
|
96
|
+
return self._model_class
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def auto_commit(self) -> bool:
|
|
100
|
+
"""是否启用自动提交。"""
|
|
101
|
+
return self._auto_commit
|
|
102
|
+
|
|
103
|
+
def with_commit(self) -> Self:
|
|
104
|
+
"""返回强制提交的 Repository 视图。
|
|
105
|
+
|
|
106
|
+
在 auto_commit=False 时,使用此方法可以强制单次操作提交。
|
|
107
|
+
|
|
108
|
+
示例:
|
|
109
|
+
repo = UserRepository(session, User, auto_commit=False)
|
|
110
|
+
await repo.create({"name": "test"}) # 只 flush
|
|
111
|
+
await repo.with_commit().create({"name": "test2"}) # 强制 commit
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Self: 带强制提交标记的 Repository 副本
|
|
115
|
+
"""
|
|
116
|
+
clone = self.__class__(
|
|
117
|
+
self._session,
|
|
118
|
+
self._model_class,
|
|
119
|
+
self._auto_commit,
|
|
120
|
+
)
|
|
121
|
+
clone._force_commit = True
|
|
122
|
+
return clone
|
|
123
|
+
|
|
124
|
+
async def _maybe_commit(self) -> None:
|
|
125
|
+
"""根据配置决定是否提交。
|
|
126
|
+
|
|
127
|
+
提交条件:
|
|
128
|
+
1. 不在显式事务中(transactional_context / @transactional)
|
|
129
|
+
2. 满足以下任一条件:
|
|
130
|
+
- _force_commit=True(来自 with_commit())
|
|
131
|
+
- _auto_commit=True(默认配置)
|
|
132
|
+
|
|
133
|
+
注意:使用 _transaction_depth 而非 session.in_transaction() 判断,
|
|
134
|
+
因为 SQLAlchemy 的 autobegin 会让 in_transaction() 在任何 SQL 执行后返回 True,
|
|
135
|
+
但这不代表用户在显式事务中。
|
|
136
|
+
"""
|
|
137
|
+
# 在显式事务中(transactional_context / @transactional),由事务统一管理
|
|
138
|
+
if _transaction_depth.get() > 0:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# 强制提交或自动提交
|
|
142
|
+
if self._force_commit or self._auto_commit:
|
|
143
|
+
await self._session.commit()
|
|
144
|
+
logger.debug("Repository 自动提交")
|
|
145
|
+
|
|
146
|
+
def query(self) -> QueryBuilder[ModelType]:
|
|
147
|
+
builder = QueryBuilder(self._model_class)
|
|
148
|
+
if hasattr(self._model_class, "deleted_at"):
|
|
149
|
+
deleted_at = _get_model_attr(self._model_class, "deleted_at")
|
|
150
|
+
builder.filter_by(deleted_at == 0)
|
|
151
|
+
return builder
|
|
152
|
+
|
|
153
|
+
def _build_base_query(self) -> Select:
|
|
154
|
+
query = select(self._model_class)
|
|
155
|
+
if hasattr(self._model_class, "deleted_at"):
|
|
156
|
+
deleted_at = _get_model_attr(self._model_class, "deleted_at")
|
|
157
|
+
query = query.where(deleted_at == 0)
|
|
158
|
+
return query
|
|
159
|
+
|
|
160
|
+
def _apply_filters(self, query: Select, **filters) -> Select:
|
|
161
|
+
"""将简单和带操作符的 filters 应用于查询。
|
|
162
|
+
|
|
163
|
+
支持键名后缀操作符(与 QueryBuilder.filter 一致):
|
|
164
|
+
- __gt, __lt, __gte, __lte, __in, __like, __ilike, __isnull, __ne
|
|
165
|
+
例如:
|
|
166
|
+
- name__ilike="%foo%"
|
|
167
|
+
- age__gte=18
|
|
168
|
+
- id__in=[1,2,3]
|
|
169
|
+
- deleted_at__isnull=True
|
|
170
|
+
|
|
171
|
+
注意:此处所有条件均以 AND 组合。更复杂的 AND/OR/NOT 组合请使用 repo.query()。
|
|
172
|
+
"""
|
|
173
|
+
for key, value in filters.items():
|
|
174
|
+
if value is None:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if "__" in key:
|
|
178
|
+
field_name, operator = key.rsplit("__", 1)
|
|
179
|
+
if not hasattr(self._model_class, field_name):
|
|
180
|
+
continue
|
|
181
|
+
field = _get_model_attr(self._model_class, field_name)
|
|
182
|
+
|
|
183
|
+
if operator == "isnull":
|
|
184
|
+
condition = field.is_(None) if bool(value) else field.isnot(None)
|
|
185
|
+
elif operator == "in":
|
|
186
|
+
condition = field.in_(value)
|
|
187
|
+
elif operator == "gt":
|
|
188
|
+
condition = field > value
|
|
189
|
+
elif operator == "lt":
|
|
190
|
+
condition = field < value
|
|
191
|
+
elif operator == "gte":
|
|
192
|
+
condition = field >= value
|
|
193
|
+
elif operator == "lte":
|
|
194
|
+
condition = field <= value
|
|
195
|
+
elif operator == "like":
|
|
196
|
+
condition = field.like(value)
|
|
197
|
+
elif operator == "ilike":
|
|
198
|
+
condition = field.ilike(value)
|
|
199
|
+
elif operator == "ne":
|
|
200
|
+
condition = field != value
|
|
201
|
+
else:
|
|
202
|
+
# 未知操作符,忽略
|
|
203
|
+
continue
|
|
204
|
+
query = query.where(condition)
|
|
205
|
+
else:
|
|
206
|
+
if hasattr(self._model_class, key):
|
|
207
|
+
query = query.where(getattr(self._model_class, key) == value)
|
|
208
|
+
return query
|
|
209
|
+
|
|
210
|
+
async def get(self, id: Union[int, GUID]) -> ModelType | None:
|
|
211
|
+
"""按 ID 获取实体,支持 int 和 GUID。"""
|
|
212
|
+
if not hasattr(self._model_class, "id"):
|
|
213
|
+
raise AttributeError(f"模型 {self._model_class.__name__} 没有 'id' 字段,请继承 IDMixin 或 UUIDMixin")
|
|
214
|
+
id_attr = _get_model_attr(self._model_class, "id")
|
|
215
|
+
query = self._build_base_query().where(id_attr == id)
|
|
216
|
+
result = await self._session.execute(query)
|
|
217
|
+
return result.scalar_one_or_none()
|
|
218
|
+
|
|
219
|
+
async def get_by(self, **filters) -> ModelType | None:
|
|
220
|
+
query = self._build_base_query()
|
|
221
|
+
query = self._apply_filters(query, **filters)
|
|
222
|
+
result = await self._session.execute(query)
|
|
223
|
+
return result.scalar_one_or_none()
|
|
224
|
+
|
|
225
|
+
async def list(self, skip: int = 0, limit: int | None = 100, **filters) -> list[ModelType]:
|
|
226
|
+
query = self._build_base_query()
|
|
227
|
+
query = self._apply_filters(query, **filters)
|
|
228
|
+
query = query.offset(skip)
|
|
229
|
+
if limit is not None:
|
|
230
|
+
query = query.limit(limit)
|
|
231
|
+
result = await self._session.execute(query)
|
|
232
|
+
return list(result.scalars().all())
|
|
233
|
+
|
|
234
|
+
async def paginate(
|
|
235
|
+
self,
|
|
236
|
+
pagination_params: PaginationParams,
|
|
237
|
+
sort_params: SortParams | None = None,
|
|
238
|
+
**filters
|
|
239
|
+
) -> PaginationResult[ModelType]:
|
|
240
|
+
query = self._build_base_query()
|
|
241
|
+
query = self._apply_filters(query, **filters)
|
|
242
|
+
|
|
243
|
+
if sort_params:
|
|
244
|
+
order_by_list = []
|
|
245
|
+
for field, direction in sort_params.sorts:
|
|
246
|
+
field_attr = getattr(self._model_class, field, None)
|
|
247
|
+
if field_attr is not None:
|
|
248
|
+
if direction == "desc":
|
|
249
|
+
order_by_list.append(field_attr.desc())
|
|
250
|
+
else:
|
|
251
|
+
order_by_list.append(field_attr)
|
|
252
|
+
if order_by_list:
|
|
253
|
+
query = query.order_by(*order_by_list)
|
|
254
|
+
|
|
255
|
+
query = query.offset(pagination_params.offset).limit(pagination_params.limit)
|
|
256
|
+
result = await self._session.execute(query)
|
|
257
|
+
items = list(result.scalars().all())
|
|
258
|
+
total_count = await self.count(**filters)
|
|
259
|
+
|
|
260
|
+
return PaginationResult.create(
|
|
261
|
+
items=items,
|
|
262
|
+
total=total_count,
|
|
263
|
+
pagination_params=pagination_params,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def count(self, **filters) -> int:
|
|
267
|
+
query = select(func.count()).select_from(self._model_class)
|
|
268
|
+
if hasattr(self._model_class, "deleted_at"):
|
|
269
|
+
deleted_at = _get_model_attr(self._model_class, "deleted_at")
|
|
270
|
+
query = query.where(deleted_at == 0)
|
|
271
|
+
query = self._apply_filters(query, **filters)
|
|
272
|
+
result = await self._session.execute(query)
|
|
273
|
+
return result.scalar_one()
|
|
274
|
+
|
|
275
|
+
async def exists(self, **filters) -> bool:
|
|
276
|
+
return await self.count(**filters) > 0
|
|
277
|
+
|
|
278
|
+
async def add(self, entity: ModelType) -> ModelType:
|
|
279
|
+
self._session.add(entity)
|
|
280
|
+
await self._session.flush()
|
|
281
|
+
await self._session.refresh(entity)
|
|
282
|
+
await self._maybe_commit()
|
|
283
|
+
logger.debug(f"添加实体: {entity}")
|
|
284
|
+
return entity
|
|
285
|
+
|
|
286
|
+
async def create(self, data: dict[str, Any]) -> ModelType:
|
|
287
|
+
entity = self._model_class(**data)
|
|
288
|
+
# add() 已调用 _maybe_commit,此处无需重复
|
|
289
|
+
return await self.add(entity)
|
|
290
|
+
|
|
291
|
+
async def update(self, entity: ModelType, data: dict[str, Any] | None = None) -> ModelType:
|
|
292
|
+
if hasattr(entity, "version"):
|
|
293
|
+
entity_any = cast(Any, entity)
|
|
294
|
+
current_version = entity_any.version
|
|
295
|
+
await self._session.refresh(entity, ["version"])
|
|
296
|
+
if entity_any.version != current_version:
|
|
297
|
+
raise VersionConflictError(
|
|
298
|
+
message="数据已被其他操作修改,请刷新后重试",
|
|
299
|
+
current_version=entity_any.version,
|
|
300
|
+
expected_version=current_version,
|
|
301
|
+
)
|
|
302
|
+
entity_any.version = entity_any.version + 1
|
|
303
|
+
|
|
304
|
+
if data:
|
|
305
|
+
for key, value in data.items():
|
|
306
|
+
if hasattr(entity, key) and key != "version":
|
|
307
|
+
setattr(entity, key, value)
|
|
308
|
+
|
|
309
|
+
await self._session.flush()
|
|
310
|
+
await self._session.refresh(entity)
|
|
311
|
+
await self._maybe_commit()
|
|
312
|
+
logger.debug(f"更新实体: {entity}")
|
|
313
|
+
return entity
|
|
314
|
+
|
|
315
|
+
async def delete(self, entity: ModelType, soft: bool = True) -> None:
|
|
316
|
+
if soft:
|
|
317
|
+
if hasattr(entity, "deleted_at"):
|
|
318
|
+
# 使用 setattr 因为类型检查器不知道动态属性
|
|
319
|
+
setattr(entity, "deleted_at", int(time.time())) # noqa: B010
|
|
320
|
+
await self._session.flush()
|
|
321
|
+
await self._maybe_commit()
|
|
322
|
+
logger.debug(f"软删除实体: {entity}")
|
|
323
|
+
elif hasattr(entity, "mark_deleted"):
|
|
324
|
+
entity.mark_deleted()
|
|
325
|
+
await self._session.flush()
|
|
326
|
+
await self._maybe_commit()
|
|
327
|
+
logger.debug(f"软删除实体(使用方法): {entity}")
|
|
328
|
+
else:
|
|
329
|
+
await self._session.delete(entity)
|
|
330
|
+
await self._session.flush()
|
|
331
|
+
await self._maybe_commit()
|
|
332
|
+
logger.debug(f"硬删除实体(无软删除字段): {entity}")
|
|
333
|
+
else:
|
|
334
|
+
await self._session.delete(entity)
|
|
335
|
+
await self._session.flush()
|
|
336
|
+
await self._maybe_commit()
|
|
337
|
+
logger.debug(f"硬删除实体: {entity}")
|
|
338
|
+
|
|
339
|
+
async def mark_deleted(self, entity: ModelType) -> None:
|
|
340
|
+
await self.delete(entity, soft=True)
|
|
341
|
+
|
|
342
|
+
async def hard_delete(self, entity: ModelType) -> None:
|
|
343
|
+
await self.delete(entity, soft=False)
|
|
344
|
+
|
|
345
|
+
async def delete_by_id(self, id: int, soft: bool = True) -> bool:
|
|
346
|
+
entity = await self.get(id)
|
|
347
|
+
if entity:
|
|
348
|
+
await self.delete(entity, soft=soft)
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
async def batch_create(self, data_list: list[dict[str, Any]]) -> list[ModelType]:
|
|
353
|
+
entities = [self._model_class(**data) for data in data_list]
|
|
354
|
+
self._session.add_all(entities)
|
|
355
|
+
await self._session.flush()
|
|
356
|
+
for entity in entities:
|
|
357
|
+
await self._session.refresh(entity)
|
|
358
|
+
await self._maybe_commit()
|
|
359
|
+
logger.debug(f"批量创建 {len(entities)} 个实体")
|
|
360
|
+
return entities
|
|
361
|
+
|
|
362
|
+
async def bulk_insert(self, data_list: list[dict[str, Any]]) -> None:
|
|
363
|
+
if not data_list:
|
|
364
|
+
return
|
|
365
|
+
await self._session.execute(
|
|
366
|
+
self._model_class.__table__.insert(),
|
|
367
|
+
data_list
|
|
368
|
+
)
|
|
369
|
+
await self._session.flush()
|
|
370
|
+
await self._maybe_commit()
|
|
371
|
+
logger.debug(f"批量插入 {len(data_list)} 条记录")
|
|
372
|
+
|
|
373
|
+
async def bulk_update(self, data_list: list[dict[str, Any]],
|
|
374
|
+
index_elements: list[str] | None = None) -> None:
|
|
375
|
+
if not data_list:
|
|
376
|
+
return
|
|
377
|
+
if index_elements is None:
|
|
378
|
+
index_elements = ["id"]
|
|
379
|
+
for data in data_list:
|
|
380
|
+
for field in index_elements:
|
|
381
|
+
if field not in data:
|
|
382
|
+
raise ValueError(f"批量更新数据缺少索引字段: {field}")
|
|
383
|
+
await self._session.bulk_update_mappings(self._model_class, data_list)
|
|
384
|
+
await self._session.flush()
|
|
385
|
+
await self._maybe_commit()
|
|
386
|
+
logger.debug(f"批量更新 {len(data_list)} 条记录")
|
|
387
|
+
|
|
388
|
+
async def bulk_delete(self, filters: dict[str, Any] | None = None) -> int:
|
|
389
|
+
query = delete(self._model_class)
|
|
390
|
+
if hasattr(self._model_class, "deleted_at"):
|
|
391
|
+
deleted_at = _get_model_attr(self._model_class, "deleted_at")
|
|
392
|
+
query = query.where(deleted_at == 0)
|
|
393
|
+
if filters:
|
|
394
|
+
for key, value in filters.items():
|
|
395
|
+
if hasattr(self._model_class, key) and value is not None:
|
|
396
|
+
attr = _get_model_attr(self._model_class, key)
|
|
397
|
+
query = query.where(attr == value)
|
|
398
|
+
result = await self._session.execute(query)
|
|
399
|
+
await self._session.flush()
|
|
400
|
+
await self._maybe_commit()
|
|
401
|
+
deleted_count = cast(Any, result).rowcount
|
|
402
|
+
logger.debug(f"批量删除 {deleted_count} 条记录")
|
|
403
|
+
return deleted_count
|
|
404
|
+
|
|
405
|
+
def __repr__(self) -> str:
|
|
406
|
+
return f"<{self.__class__.__name__} model={self._model_class.__name__}>"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SimpleRepository:
|
|
410
|
+
"""简单Repository基类。"""
|
|
411
|
+
|
|
412
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
413
|
+
self._session = session
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def session(self) -> AsyncSession:
|
|
417
|
+
return self._session
|
|
418
|
+
|
|
419
|
+
def __repr__(self) -> str:
|
|
420
|
+
return f"<{self.__class__.__name__}>"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
__all__ = ["BaseRepository", "SimpleRepository"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""查询拦截器接口。
|
|
2
|
+
|
|
3
|
+
用于在查询执行前后执行自定义逻辑,如审计、日志记录等。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import Select
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QueryInterceptor(ABC):
|
|
15
|
+
"""查询拦截器接口。
|
|
16
|
+
|
|
17
|
+
用于在查询执行前后执行自定义逻辑,如审计、日志记录等。
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
class AuditInterceptor(QueryInterceptor):
|
|
21
|
+
async def before_query(self, query, **kwargs):
|
|
22
|
+
logger.info(f"执行查询: {query}")
|
|
23
|
+
|
|
24
|
+
async def after_query(self, result, **kwargs):
|
|
25
|
+
logger.info(f"查询结果: {len(result)} 条记录")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def before_query(self, query: Select, **kwargs: Any) -> None:
|
|
30
|
+
"""查询执行前的钩子。
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
query: SQLAlchemy 查询对象
|
|
34
|
+
**kwargs: 其他参数
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def after_query(self, result: Any, **kwargs: Any) -> None:
|
|
40
|
+
"""查询执行后的钩子。
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
result: 查询结果
|
|
44
|
+
**kwargs: 其他参数
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""仓储接口定义。
|
|
2
|
+
|
|
3
|
+
定义所有Repository必须实现的接口。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from aury.boot.domain.models import Base, GUID
|
|
12
|
+
from aury.boot.domain.pagination import PaginationParams, PaginationResult, SortParams
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Self
|
|
16
|
+
|
|
17
|
+
from aury.boot.domain.repository.query_builder import QueryBuilder
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IRepository[ModelType: Base](ABC):
|
|
21
|
+
"""仓储接口定义。
|
|
22
|
+
|
|
23
|
+
定义所有Repository必须实现的方法。
|
|
24
|
+
遵循接口隔离原则,只定义必要的方法。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def get(self, id: int | GUID) -> ModelType | None:
|
|
29
|
+
"""根据ID获取实体。支持 int 和 GUID。"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def get_by(self, **filters) -> ModelType | None:
|
|
34
|
+
"""根据条件获取单个实体。"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def list(self, skip: int = 0, limit: int | None = 100, **filters) -> list[ModelType]:
|
|
39
|
+
"""获取实体列表。limit=None 时返回全部记录(谨慎使用)。"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def paginate(
|
|
44
|
+
self,
|
|
45
|
+
pagination_params: PaginationParams,
|
|
46
|
+
sort_params: SortParams | None = None,
|
|
47
|
+
**filters
|
|
48
|
+
) -> PaginationResult[ModelType]:
|
|
49
|
+
"""分页获取实体列表。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
pagination_params: 分页参数
|
|
53
|
+
sort_params: 排序参数
|
|
54
|
+
**filters: 过滤条件
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
PaginationResult[ModelType]: 分页结果
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def count(self, **filters) -> int:
|
|
63
|
+
"""统计实体数量。"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def exists(self, **filters) -> bool:
|
|
68
|
+
"""检查实体是否存在。"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def add(self, entity: ModelType) -> ModelType:
|
|
73
|
+
"""添加实体。"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def create(self, data: dict[str, Any]) -> ModelType:
|
|
78
|
+
"""创建实体。"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def update(self, entity: ModelType, data: dict[str, Any]) -> ModelType:
|
|
83
|
+
"""更新实体。"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def delete(self, entity: ModelType, soft: bool = True) -> None:
|
|
88
|
+
"""删除实体。"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def query(self) -> QueryBuilder[ModelType]:
|
|
93
|
+
"""创建查询构建器。"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def with_commit(self) -> Self:
|
|
98
|
+
"""返回强制提交的 Repository 视图。
|
|
99
|
+
|
|
100
|
+
在 auto_commit=False 时,使用此方法可以强制单次操作提交。
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Self: 带强制提交标记的 Repository 副本
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|