aiteamutils 0.2.51__py3-none-any.whl → 0.2.52__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils/base_repository.py +68 -364
- aiteamutils/base_service.py +46 -43
- aiteamutils/database.py +24 -5
- aiteamutils/dependencies.py +183 -105
- aiteamutils/security.py +52 -289
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.52.dist-info}/METADATA +1 -1
- aiteamutils-0.2.52.dist-info/RECORD +16 -0
- aiteamutils-0.2.51.dist-info/RECORD +0 -16
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.52.dist-info}/WHEEL +0 -0
aiteamutils/base_repository.py
CHANGED
@@ -3,281 +3,54 @@ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
|
|
3
3
|
from sqlalchemy.orm import DeclarativeBase, Load
|
4
4
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
5
5
|
from sqlalchemy import select, or_, and_
|
6
|
-
from .database import DatabaseService
|
7
6
|
from .exceptions import CustomException, ErrorCode
|
8
7
|
from sqlalchemy.orm import joinedload
|
9
8
|
from sqlalchemy.sql import Select
|
10
9
|
from fastapi import Request
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
11
11
|
|
12
12
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
13
13
|
|
14
14
|
class BaseRepository(Generic[ModelType]):
|
15
|
-
|
15
|
+
##################
|
16
16
|
# 1. 초기화 영역 #
|
17
17
|
##################
|
18
|
-
def __init__(self,
|
18
|
+
def __init__(self, session: AsyncSession, model: Type[ModelType]):
|
19
19
|
"""
|
20
20
|
Args:
|
21
|
-
|
21
|
+
session (AsyncSession): 데이터베이스 세션
|
22
22
|
model (Type[ModelType]): 모델 클래스
|
23
23
|
"""
|
24
|
-
self.
|
24
|
+
self.session = session
|
25
25
|
self.model = model
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
"""
|
36
|
-
return select(self.model)
|
37
|
-
|
38
|
-
#######################
|
39
|
-
# 3. 전차리 영역 #
|
40
|
-
#######################
|
41
|
-
def _apply_exact_match(self, stmt: Select, field_name: str, value: Any) -> Select:
|
42
|
-
"""정확한 값 매칭 조건을 적용합니다.
|
43
|
-
|
44
|
-
Args:
|
45
|
-
stmt (Select): 쿼리문
|
46
|
-
field_name (str): 필드명
|
47
|
-
value (Any): 매칭할 값
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
Select: 조건이 적용된 쿼리
|
51
|
-
"""
|
52
|
-
return stmt.where(getattr(self.model, field_name) == value)
|
53
|
-
|
54
|
-
def _apply_like_match(self, stmt: Select, field_name: str, value: str) -> Select:
|
55
|
-
"""LIKE 검색 조건을 적용합니다.
|
56
|
-
|
57
|
-
Args:
|
58
|
-
stmt (Select): 쿼리문
|
59
|
-
field_name (str): 필드명
|
60
|
-
value (str): 검색할 값
|
61
|
-
|
62
|
-
Returns:
|
63
|
-
Select: 조건이 적용된 쿼리
|
64
|
-
"""
|
65
|
-
return stmt.where(getattr(self.model, field_name).ilike(f"%{value}%"))
|
66
|
-
|
67
|
-
def _apply_relation_match(self, stmt: Select, relations: List[str], field_name: str, operator: str, value: Any) -> Select:
|
68
|
-
"""관계 테이블 검색 조건을 적용합니다."""
|
69
|
-
current = self.model
|
70
|
-
|
71
|
-
# 관계 체인 따라가기
|
72
|
-
for i in range(len(relations)-1):
|
73
|
-
current = getattr(current, relations[i]).property.mapper.class_
|
74
|
-
|
75
|
-
# 마지막 모델과 필드
|
76
|
-
final_model = getattr(current, relations[-1]).property.mapper.class_
|
77
|
-
|
78
|
-
# 중첩된 EXISTS 절 생성
|
79
|
-
current = self.model
|
80
|
-
subq = select(1)
|
81
|
-
|
82
|
-
# 첫 번째 관계
|
83
|
-
next_model = getattr(current, relations[0]).property.mapper.class_
|
84
|
-
subq = subq.where(getattr(next_model, 'ulid') == getattr(current, f"{relations[0]}_ulid"))
|
85
|
-
|
86
|
-
# 중간 관계들
|
87
|
-
for i in range(1, len(relations)):
|
88
|
-
prev_model = next_model
|
89
|
-
next_model = getattr(prev_model, relations[i]).property.mapper.class_
|
90
|
-
subq = subq.where(getattr(next_model, 'ulid') == getattr(prev_model, f"{relations[i]}_ulid"))
|
91
|
-
|
92
|
-
# 최종 검색 조건
|
93
|
-
subq = subq.where(getattr(final_model, field_name).__getattribute__(operator)(value))
|
94
|
-
|
95
|
-
return stmt.where(subq.exists())
|
96
|
-
|
97
|
-
def _apply_ordering(self, stmt: Select, order_by: List[str]) -> Select:
|
98
|
-
"""정렬 조건을 적용합니다.
|
99
|
-
|
100
|
-
Args:
|
101
|
-
stmt (Select): 쿼리문
|
102
|
-
order_by (List[str]): 정렬 기준 필드 목록 (예: ["name", "-created_at"])
|
103
|
-
|
104
|
-
Returns:
|
105
|
-
Select: 정렬이 적용된 쿼리
|
106
|
-
"""
|
107
|
-
for field in order_by:
|
108
|
-
if field.startswith("-"):
|
109
|
-
field_name = field[1:]
|
110
|
-
stmt = stmt.order_by(getattr(self.model, field_name).desc())
|
111
|
-
else:
|
112
|
-
stmt = stmt.order_by(getattr(self.model, field).asc())
|
113
|
-
return stmt
|
114
|
-
|
115
|
-
def _apply_pagination(self, stmt: Select, skip: int = 0, limit: int = 100) -> Select:
|
116
|
-
"""페이징을 적용합니다.
|
117
|
-
|
118
|
-
Args:
|
119
|
-
stmt (Select): 쿼리문
|
120
|
-
skip (int): 건너뛸 레코드 수
|
121
|
-
limit (int): 조회할 최대 레코드 수
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
Select: 페이징이 적용된 쿼리
|
125
|
-
"""
|
126
|
-
return stmt.offset(skip).limit(limit)
|
127
|
-
|
128
|
-
def _apply_joins(self, stmt: Select, joins: List[str]) -> Select:
|
129
|
-
"""조인을 적용합니다.
|
130
|
-
|
131
|
-
Args:
|
132
|
-
stmt (Select): 쿼리문
|
133
|
-
joins (List[str]): 조인할 관계명 목록
|
134
|
-
|
135
|
-
Returns:
|
136
|
-
Select: 조인이 적용된 쿼리
|
137
|
-
"""
|
138
|
-
for join in joins:
|
139
|
-
stmt = stmt.options(joinedload(getattr(self.model, join)))
|
140
|
-
return stmt
|
141
|
-
|
142
|
-
def _build_jsonb_condition(self, model: Any, field_path: str, value: str) -> Any:
|
143
|
-
"""JSONB 필드에 대한 검색 조건을 생성합니다.
|
144
|
-
|
145
|
-
Args:
|
146
|
-
model: 대상 모델
|
147
|
-
field_path (str): JSONB 키 경로 (예: "address", "name.first")
|
148
|
-
value (str): 검색할 값
|
149
|
-
|
150
|
-
Returns:
|
151
|
-
Any: SQLAlchemy 검색 조건
|
152
|
-
"""
|
153
|
-
# JSONB 경로가 중첩된 경우 (예: "name.first")
|
154
|
-
if "." in field_path:
|
155
|
-
path_parts = field_path.split(".")
|
156
|
-
jsonb_path = "{" + ",".join(path_parts) + "}"
|
157
|
-
return model.extra_data[jsonb_path].astext.ilike(f"%{value}%")
|
158
|
-
# 단일 키인 경우
|
159
|
-
return model.extra_data[field_path].astext.ilike(f"%{value}%")
|
160
|
-
|
161
|
-
def _apply_jsonb_match(self, stmt: Select, relations: List[str], json_key: str, value: str) -> Select:
|
162
|
-
"""JSONB 필드 검색 조건을 적용합니다.
|
163
|
-
|
164
|
-
Args:
|
165
|
-
stmt (Select): 쿼리문
|
166
|
-
relations (List[str]): 관계 테이블 경로
|
167
|
-
json_key (str): JSONB 키 경로
|
168
|
-
value (str): 검색할 값
|
169
|
-
|
170
|
-
Returns:
|
171
|
-
Select: 조건이 적용된 쿼리
|
172
|
-
"""
|
173
|
-
current = self.model
|
174
|
-
|
175
|
-
# 단일 모델 검색
|
176
|
-
if not relations:
|
177
|
-
condition = self._build_jsonb_condition(current, json_key, value)
|
178
|
-
return stmt.where(condition)
|
179
|
-
|
180
|
-
# 관계 모델 검색
|
181
|
-
for i in range(len(relations)-1):
|
182
|
-
current = getattr(current, relations[i]).property.mapper.class_
|
183
|
-
|
184
|
-
final_model = getattr(current, relations[-1]).property.mapper.class_
|
185
|
-
|
186
|
-
# 관계 체인 구성
|
187
|
-
if len(relations) == 1:
|
188
|
-
condition = getattr(self.model, relations[0]).has(
|
189
|
-
self._build_jsonb_condition(final_model, json_key, value)
|
190
|
-
)
|
191
|
-
else:
|
192
|
-
condition = getattr(self.model, relations[0]).has(
|
193
|
-
getattr(final_model, relations[-1]).has(
|
194
|
-
self._build_jsonb_condition(final_model, json_key, value)
|
195
|
-
)
|
26
|
+
|
27
|
+
@property
|
28
|
+
def session(self):
|
29
|
+
"""현재 세션을 반환합니다."""
|
30
|
+
if self._session is None:
|
31
|
+
raise CustomException(
|
32
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
33
|
+
detail="Database session is not set",
|
34
|
+
source_function=f"{self.__class__.__name__}.session"
|
196
35
|
)
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
def
|
201
|
-
"""
|
202
|
-
|
203
|
-
return stmt
|
204
|
-
|
205
|
-
for key, value in search_params.items():
|
206
|
-
if not value.get("value"):
|
207
|
-
continue
|
208
|
-
|
209
|
-
conditions = []
|
210
|
-
for field in value.get("fields", []):
|
211
|
-
parts = field.split('.')
|
212
|
-
|
213
|
-
if len(parts) == 1:
|
214
|
-
# 직접 필드 검색
|
215
|
-
condition = self._apply_like_match(stmt, parts[0], value["value"]).whereclause
|
216
|
-
elif 'extra_data' in parts:
|
217
|
-
# JSONB 필드 검색
|
218
|
-
extra_data_idx = parts.index('extra_data')
|
219
|
-
tables = parts[:extra_data_idx]
|
220
|
-
json_key = ".".join(parts[extra_data_idx + 1:])
|
221
|
-
condition = self._apply_jsonb_match(
|
222
|
-
stmt,
|
223
|
-
tables,
|
224
|
-
json_key,
|
225
|
-
value["value"]
|
226
|
-
).whereclause
|
227
|
-
else:
|
228
|
-
# 관계 테이블 검색
|
229
|
-
condition = self._apply_relation_match(
|
230
|
-
stmt,
|
231
|
-
parts[:-1],
|
232
|
-
parts[-1],
|
233
|
-
"ilike",
|
234
|
-
f"%{value['value']}%"
|
235
|
-
).whereclause
|
236
|
-
|
237
|
-
conditions.append(condition)
|
238
|
-
|
239
|
-
if conditions:
|
240
|
-
stmt = stmt.where(or_(*conditions))
|
241
|
-
|
242
|
-
return stmt
|
243
|
-
|
244
|
-
def _apply_filters(self, stmt, filters: Dict[str, Any]):
|
245
|
-
"""일반 필터를 적용합니다."""
|
246
|
-
for key, value in filters.items():
|
247
|
-
if value is None:
|
248
|
-
continue
|
249
|
-
|
250
|
-
if "." in key:
|
251
|
-
# 관계 테이블 필터
|
252
|
-
relation, field = key.split(".")
|
253
|
-
stmt = self._apply_relation_match(stmt, relation, field, "__eq__", value)
|
254
|
-
else:
|
255
|
-
# 일반 필드 필터
|
256
|
-
stmt = stmt.where(getattr(self.model, key) == value)
|
257
|
-
|
258
|
-
return stmt
|
36
|
+
return self._session
|
37
|
+
|
38
|
+
@session.setter
|
39
|
+
def session(self, value):
|
40
|
+
"""세션을 설정합니다."""
|
41
|
+
self._session = value
|
259
42
|
|
260
43
|
#######################
|
261
|
-
#
|
44
|
+
# 2. CRUD 작업 #
|
262
45
|
#######################
|
263
46
|
async def get(
|
264
47
|
self,
|
265
48
|
ulid: str
|
266
49
|
) -> Optional[Dict[str, Any]]:
|
267
|
-
"""ULID로 엔티티를 조회합니다.
|
268
|
-
|
269
|
-
Args:
|
270
|
-
ulid (str): 조회할 엔티티의 ULID
|
271
|
-
|
272
|
-
Returns:
|
273
|
-
Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
|
274
|
-
|
275
|
-
Raises:
|
276
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
277
|
-
"""
|
50
|
+
"""ULID로 엔티티를 조회합니다."""
|
278
51
|
try:
|
279
52
|
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
280
|
-
result = await self.
|
53
|
+
result = await self.session.execute(stmt)
|
281
54
|
entity = result.scalars().unique().first()
|
282
55
|
|
283
56
|
if not entity:
|
@@ -298,14 +71,7 @@ class BaseRepository(Generic[ModelType]):
|
|
298
71
|
source_function=f"{self.__class__.__name__}.get",
|
299
72
|
original_error=e
|
300
73
|
)
|
301
|
-
|
302
|
-
raise CustomException(
|
303
|
-
ErrorCode.DB_QUERY_ERROR,
|
304
|
-
detail=f"Unexpected repository error in {self.model.__tablename__}: {str(e)}",
|
305
|
-
source_function=f"{self.__class__.__name__}.get",
|
306
|
-
original_error=e
|
307
|
-
)
|
308
|
-
|
74
|
+
|
309
75
|
async def list(
|
310
76
|
self,
|
311
77
|
skip: int = 0,
|
@@ -328,7 +94,7 @@ class BaseRepository(Generic[ModelType]):
|
|
328
94
|
# 페이지네이션 적용
|
329
95
|
stmt = stmt.limit(limit).offset(skip)
|
330
96
|
|
331
|
-
result = await self.
|
97
|
+
result = await self.session.execute(stmt)
|
332
98
|
return result.scalars().unique().all()
|
333
99
|
|
334
100
|
except SQLAlchemyError as e:
|
@@ -340,165 +106,103 @@ class BaseRepository(Generic[ModelType]):
|
|
340
106
|
)
|
341
107
|
|
342
108
|
async def create(self, data: Dict[str, Any]) -> ModelType:
|
343
|
-
"""새로운 엔티티를 생성합니다.
|
344
|
-
|
345
|
-
Args:
|
346
|
-
data (Dict[str, Any]): 생성할 엔티티 데이터
|
347
|
-
|
348
|
-
Returns:
|
349
|
-
ModelType: 생성된 엔티티
|
350
|
-
|
351
|
-
Raises:
|
352
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
353
|
-
"""
|
109
|
+
"""새로운 엔티티를 생성합니다."""
|
354
110
|
try:
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
111
|
+
entity = self.model(**data)
|
112
|
+
self.session.add(entity)
|
113
|
+
await self.session.flush()
|
114
|
+
await self.session.refresh(entity)
|
115
|
+
return entity
|
360
116
|
except IntegrityError as e:
|
117
|
+
await self.session.rollback()
|
361
118
|
self._handle_integrity_error(e, "create", data)
|
362
119
|
except SQLAlchemyError as e:
|
120
|
+
await self.session.rollback()
|
363
121
|
raise CustomException(
|
364
122
|
ErrorCode.DB_CREATE_ERROR,
|
365
123
|
detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
|
366
124
|
source_function=f"{self.__class__.__name__}.create",
|
367
125
|
original_error=e
|
368
126
|
)
|
369
|
-
except Exception as e:
|
370
|
-
raise CustomException(
|
371
|
-
ErrorCode.DB_CREATE_ERROR,
|
372
|
-
detail=f"Unexpected repository create error in {self.model.__tablename__}: {str(e)}",
|
373
|
-
source_function=f"{self.__class__.__name__}.create",
|
374
|
-
original_error=e
|
375
|
-
)
|
376
127
|
|
377
128
|
async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
|
378
|
-
"""기존 엔티티를 수정합니다.
|
379
|
-
|
380
|
-
Args:
|
381
|
-
ulid (str): 수정할 엔티티의 ULID
|
382
|
-
data (Dict[str, Any]): 수정할 데이터
|
383
|
-
|
384
|
-
Returns:
|
385
|
-
Optional[ModelType]: 수정된 엔티티, 없으면 None
|
386
|
-
|
387
|
-
Raises:
|
388
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
389
|
-
"""
|
129
|
+
"""기존 엔티티를 수정합니다."""
|
390
130
|
try:
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
)
|
131
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
132
|
+
result = await self.session.execute(stmt)
|
133
|
+
entity = result.scalars().first()
|
134
|
+
|
396
135
|
if not entity:
|
397
136
|
raise CustomException(
|
398
137
|
ErrorCode.DB_NO_RESULT,
|
399
138
|
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
400
139
|
source_function=f"{self.__class__.__name__}.update"
|
401
140
|
)
|
141
|
+
|
142
|
+
for key, value in data.items():
|
143
|
+
setattr(entity, key, value)
|
144
|
+
|
145
|
+
await self.session.flush()
|
146
|
+
await self.session.refresh(entity)
|
402
147
|
return entity
|
403
|
-
|
404
|
-
e.detail = f"Repository update error for {self.model.__tablename__}: {e.detail}"
|
405
|
-
e.source_function = f"{self.__class__.__name__}.update -> {e.source_function}"
|
406
|
-
raise e
|
148
|
+
|
407
149
|
except IntegrityError as e:
|
150
|
+
await self.session.rollback()
|
408
151
|
self._handle_integrity_error(e, "update", data)
|
409
152
|
except SQLAlchemyError as e:
|
153
|
+
await self.session.rollback()
|
410
154
|
raise CustomException(
|
411
155
|
ErrorCode.DB_UPDATE_ERROR,
|
412
156
|
detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
|
413
157
|
source_function=f"{self.__class__.__name__}.update",
|
414
158
|
original_error=e
|
415
159
|
)
|
416
|
-
except Exception as e:
|
417
|
-
raise CustomException(
|
418
|
-
ErrorCode.DB_UPDATE_ERROR,
|
419
|
-
detail=f"Unexpected repository update error in {self.model.__tablename__}: {str(e)}",
|
420
|
-
source_function=f"{self.__class__.__name__}.update",
|
421
|
-
original_error=e
|
422
|
-
)
|
423
160
|
|
424
161
|
async def delete(self, ulid: str) -> bool:
|
425
|
-
"""엔티티를 소프트
|
426
|
-
|
427
|
-
Args:
|
428
|
-
ulid (str): 삭제할 엔티티의 ULID
|
429
|
-
|
430
|
-
Returns:
|
431
|
-
bool: 삭제 성공 여부
|
432
|
-
|
433
|
-
Raises:
|
434
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
435
|
-
"""
|
162
|
+
"""엔티티를 소프트 삭제합니다."""
|
436
163
|
try:
|
437
|
-
|
164
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
165
|
+
result = await self.session.execute(stmt)
|
166
|
+
entity = result.scalars().first()
|
167
|
+
|
438
168
|
if not entity:
|
439
169
|
raise CustomException(
|
440
170
|
ErrorCode.DB_NO_RESULT,
|
441
171
|
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
442
172
|
source_function=f"{self.__class__.__name__}.delete"
|
443
173
|
)
|
174
|
+
|
175
|
+
entity.is_deleted = True
|
176
|
+
await self.session.flush()
|
444
177
|
return True
|
445
|
-
|
446
|
-
e.detail = f"Repository delete error for {self.model.__tablename__}: {e.detail}"
|
447
|
-
e.source_function = f"{self.__class__.__name__}.delete -> {e.source_function}"
|
448
|
-
raise e
|
449
|
-
except IntegrityError as e:
|
450
|
-
self._handle_integrity_error(e, "delete")
|
178
|
+
|
451
179
|
except SQLAlchemyError as e:
|
180
|
+
await self.session.rollback()
|
452
181
|
raise CustomException(
|
453
182
|
ErrorCode.DB_DELETE_ERROR,
|
454
183
|
detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
|
455
184
|
source_function=f"{self.__class__.__name__}.delete",
|
456
185
|
original_error=e
|
457
186
|
)
|
458
|
-
except Exception as e:
|
459
|
-
raise CustomException(
|
460
|
-
ErrorCode.DB_DELETE_ERROR,
|
461
|
-
detail=f"Unexpected repository delete error in {self.model.__tablename__}: {str(e)}",
|
462
|
-
source_function=f"{self.__class__.__name__}.delete",
|
463
|
-
original_error=e
|
464
|
-
)
|
465
|
-
|
466
|
-
async def real_row_delete(self, ulid: str) -> bool:
|
467
|
-
"""엔티티를 실제로 삭제합니다.
|
468
|
-
|
469
|
-
Args:
|
470
|
-
ulid (str): 삭제할 엔티티의 ULID
|
471
187
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
Raises:
|
476
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
477
|
-
"""
|
188
|
+
async def real_delete(self, ulid: str) -> bool:
|
189
|
+
"""엔티티를 실제로 삭제합니다."""
|
478
190
|
try:
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
191
|
+
stmt = select(self.model).filter_by(ulid=ulid)
|
192
|
+
result = await self.session.execute(stmt)
|
193
|
+
entity = result.scalars().first()
|
194
|
+
|
483
195
|
if entity:
|
484
|
-
await self.
|
196
|
+
await self.session.delete(entity)
|
197
|
+
await self.session.flush()
|
485
198
|
return True
|
486
199
|
return False
|
487
|
-
|
488
|
-
e.detail = f"Repository real delete error for {self.model.__tablename__}: {e.detail}"
|
489
|
-
e.source_function = f"{self.__class__.__name__}.real_row_delete -> {e.source_function}"
|
490
|
-
raise e
|
200
|
+
|
491
201
|
except SQLAlchemyError as e:
|
202
|
+
await self.session.rollback()
|
492
203
|
raise CustomException(
|
493
204
|
ErrorCode.DB_DELETE_ERROR,
|
494
205
|
detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
|
495
|
-
source_function=f"{self.__class__.__name__}.
|
496
|
-
original_error=e
|
497
|
-
)
|
498
|
-
except Exception as e:
|
499
|
-
raise CustomException(
|
500
|
-
ErrorCode.DB_DELETE_ERROR,
|
501
|
-
detail=f"Unexpected repository real delete error in {self.model.__tablename__}: {str(e)}",
|
502
|
-
source_function=f"{self.__class__.__name__}.real_row_delete",
|
206
|
+
source_function=f"{self.__class__.__name__}.real_delete",
|
503
207
|
original_error=e
|
504
208
|
)
|
aiteamutils/base_service.py
CHANGED
@@ -9,6 +9,7 @@ from .base_repository import BaseRepository
|
|
9
9
|
from .security import hash_password
|
10
10
|
from fastapi import Request
|
11
11
|
from ulid import ULID
|
12
|
+
from sqlalchemy import select
|
12
13
|
|
13
14
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
15
|
|
@@ -30,11 +31,29 @@ class BaseService(Generic[ModelType]):
|
|
30
31
|
self.repository = repository
|
31
32
|
self.model = repository.model
|
32
33
|
self.additional_models = additional_models or {}
|
33
|
-
self.
|
34
|
+
self._session = None
|
34
35
|
self.searchable_fields = {
|
35
36
|
"name": {"type": "text", "description": "이름"},
|
36
37
|
"organization_ulid": {"type": "exact", "description": "조직 ID"}
|
37
38
|
}
|
39
|
+
|
40
|
+
@property
|
41
|
+
def session(self):
|
42
|
+
"""현재 세션을 반환합니다."""
|
43
|
+
if self._session is None:
|
44
|
+
raise CustomException(
|
45
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
46
|
+
detail="Database session is not set",
|
47
|
+
source_function=f"{self.__class__.__name__}.session"
|
48
|
+
)
|
49
|
+
return self._session
|
50
|
+
|
51
|
+
@session.setter
|
52
|
+
def session(self, value):
|
53
|
+
"""세션을 설정합니다."""
|
54
|
+
self._session = value
|
55
|
+
if hasattr(self.repository, 'session'):
|
56
|
+
self.repository.session = value
|
38
57
|
|
39
58
|
#########################
|
40
59
|
# 2. 이벤트 처리 메서드 #
|
@@ -448,18 +467,7 @@ class BaseService(Generic[ModelType]):
|
|
448
467
|
)
|
449
468
|
|
450
469
|
async def delete(self, ulid: str, model_name: str = None) -> bool:
|
451
|
-
"""엔티티를 소프트 삭제합니다 (is_deleted = True).
|
452
|
-
|
453
|
-
Args:
|
454
|
-
ulid (str): 삭제할 엔티티의 ULID
|
455
|
-
model_name (str, optional): 삭제할 모델 이름. Defaults to None.
|
456
|
-
|
457
|
-
Returns:
|
458
|
-
bool: 삭제 성공 여부
|
459
|
-
|
460
|
-
Raises:
|
461
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
462
|
-
"""
|
470
|
+
"""엔티티를 소프트 삭제합니다 (is_deleted = True)."""
|
463
471
|
try:
|
464
472
|
if model_name:
|
465
473
|
if model_name not in self.additional_models:
|
@@ -468,23 +476,24 @@ class BaseService(Generic[ModelType]):
|
|
468
476
|
detail=f"Model {model_name} not registered",
|
469
477
|
source_function=f"{self.__class__.__name__}.delete"
|
470
478
|
)
|
471
|
-
|
479
|
+
|
480
|
+
stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
|
481
|
+
result = await self.session.execute(stmt)
|
482
|
+
entity = result.scalars().first()
|
483
|
+
|
472
484
|
if not entity:
|
473
485
|
raise CustomException(
|
474
486
|
ErrorCode.NOT_FOUND,
|
475
487
|
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
476
488
|
source_function=f"{self.__class__.__name__}.delete"
|
477
489
|
)
|
490
|
+
|
491
|
+
entity.is_deleted = True
|
492
|
+
await self.session.flush()
|
478
493
|
return True
|
479
494
|
|
480
|
-
|
481
|
-
|
482
|
-
raise CustomException(
|
483
|
-
ErrorCode.NOT_FOUND,
|
484
|
-
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
485
|
-
source_function=f"{self.__class__.__name__}.delete"
|
486
|
-
)
|
487
|
-
return True
|
495
|
+
return await self.repository.delete(ulid)
|
496
|
+
|
488
497
|
except CustomException as e:
|
489
498
|
raise e
|
490
499
|
except Exception as e:
|
@@ -549,20 +558,7 @@ class BaseService(Generic[ModelType]):
|
|
549
558
|
request: Request | None = None,
|
550
559
|
response_model: Any = None
|
551
560
|
) -> List[Dict[str, Any]]:
|
552
|
-
"""엔티티 목록을 조회합니다.
|
553
|
-
|
554
|
-
Args:
|
555
|
-
skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
|
556
|
-
limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
|
557
|
-
filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
|
558
|
-
search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
|
559
|
-
model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
|
560
|
-
request (Request | None, optional): 요청 객체. Defaults to None.
|
561
|
-
response_model (Any, optional): 응답 스키마. Defaults to None.
|
562
|
-
|
563
|
-
Returns:
|
564
|
-
List[Dict[str, Any]]: 엔티티 목록
|
565
|
-
"""
|
561
|
+
"""엔티티 목록을 조회합니다."""
|
566
562
|
try:
|
567
563
|
if model_name:
|
568
564
|
if model_name not in self.additional_models:
|
@@ -571,21 +567,28 @@ class BaseService(Generic[ModelType]):
|
|
571
567
|
detail=f"Model {model_name} not registered",
|
572
568
|
source_function=f"{self.__class__.__name__}.list"
|
573
569
|
)
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
limit=limit,
|
578
|
-
filters=filters
|
570
|
+
|
571
|
+
stmt = select(self.additional_models[model_name]).where(
|
572
|
+
self.additional_models[model_name].is_deleted == False
|
579
573
|
)
|
574
|
+
|
575
|
+
if filters:
|
576
|
+
for key, value in filters.items():
|
577
|
+
if value is not None:
|
578
|
+
stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
|
579
|
+
|
580
|
+
stmt = stmt.offset(skip).limit(limit)
|
581
|
+
result = await self.session.execute(stmt)
|
582
|
+
entities = result.scalars().all()
|
583
|
+
|
580
584
|
return [self._process_response(entity, response_model) for entity in entities]
|
581
585
|
|
582
|
-
|
586
|
+
return await self.repository.list(
|
583
587
|
skip=skip,
|
584
588
|
limit=limit,
|
585
589
|
filters=filters,
|
586
590
|
search_params=search_params
|
587
591
|
)
|
588
|
-
return [self._process_response(entity, response_model) for entity in entities]
|
589
592
|
|
590
593
|
except CustomException as e:
|
591
594
|
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|