aiteamutils 0.2.53__py3-none-any.whl → 0.2.55__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.
- aiteamutils/__init__.py +1 -11
- aiteamutils/database.py +111 -987
- aiteamutils/dependencies.py +7 -24
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.55.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.55.dist-info}/RECORD +7 -7
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.55.dist-info}/WHEEL +0 -0
aiteamutils/__init__.py
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
from .base_model import Base
|
2
|
-
from .database import
|
3
|
-
DatabaseService,
|
4
|
-
DatabaseServiceManager,
|
5
|
-
get_db,
|
6
|
-
get_database_service,
|
7
|
-
lifespan
|
8
|
-
)
|
2
|
+
from .database import DatabaseService
|
9
3
|
from .exceptions import (
|
10
4
|
CustomException,
|
11
5
|
ErrorCode,
|
@@ -36,10 +30,6 @@ __all__ = [
|
|
36
30
|
|
37
31
|
# Database
|
38
32
|
"DatabaseService",
|
39
|
-
"DatabaseServiceManager",
|
40
|
-
"get_db",
|
41
|
-
"get_database_service",
|
42
|
-
"lifespan",
|
43
33
|
|
44
34
|
# Exceptions
|
45
35
|
"CustomException",
|
aiteamutils/database.py
CHANGED
@@ -1,1083 +1,207 @@
|
|
1
|
-
|
2
|
-
import
|
3
|
-
from
|
4
|
-
from sqlalchemy import
|
5
|
-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, AsyncEngine
|
6
|
-
from sqlalchemy.orm import sessionmaker, Load, joinedload
|
1
|
+
"""데이터베이스 유틸리티 모듈."""
|
2
|
+
from typing import Any, Dict, Optional, Type, List, Union
|
3
|
+
from sqlalchemy import select, and_
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
7
5
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
8
|
-
from sqlalchemy.pool import QueuePool
|
9
|
-
from contextlib import asynccontextmanager
|
10
|
-
from sqlalchemy import or_
|
11
|
-
from fastapi import Request, Depends, FastAPI
|
12
|
-
from ulid import ULID
|
13
|
-
from sqlalchemy.sql import Select
|
14
6
|
|
15
7
|
from .exceptions import ErrorCode, CustomException
|
16
|
-
from .base_model import Base
|
17
|
-
from .enums import ActivityType
|
18
|
-
|
19
|
-
T = TypeVar("T", bound=BaseColumn)
|
20
|
-
|
21
|
-
class DatabaseServiceManager:
|
22
|
-
_instance: Optional['DatabaseService'] = None
|
23
|
-
_lock = asyncio.Lock()
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
async def get_instance(
|
27
|
-
cls,
|
28
|
-
db_url: str = None,
|
29
|
-
db_echo: bool = False,
|
30
|
-
db_pool_size: int = 5,
|
31
|
-
db_max_overflow: int = 10,
|
32
|
-
db_pool_timeout: int = 30,
|
33
|
-
db_pool_recycle: int = 1800,
|
34
|
-
**kwargs
|
35
|
-
) -> 'DatabaseService':
|
36
|
-
"""데이터베이스 서비스의 싱글톤 인스턴스를 반환합니다.
|
37
|
-
|
38
|
-
Args:
|
39
|
-
db_url (str, optional): 데이터베이스 URL
|
40
|
-
db_echo (bool, optional): SQL 로깅 여부
|
41
|
-
db_pool_size (int, optional): DB 커넥션 풀 크기
|
42
|
-
db_max_overflow (int, optional): 최대 초과 커넥션 수
|
43
|
-
db_pool_timeout (int, optional): 커넥션 풀 타임아웃
|
44
|
-
db_pool_recycle (int, optional): 커넥션 재활용 시간
|
45
|
-
|
46
|
-
Returns:
|
47
|
-
DatabaseService: 데이터베이스 서비스 인스턴스
|
48
|
-
|
49
|
-
Raises:
|
50
|
-
CustomException: 데이터베이스 초기화 실패 시
|
51
|
-
"""
|
52
|
-
async with cls._lock:
|
53
|
-
if not cls._instance:
|
54
|
-
if not db_url:
|
55
|
-
raise CustomException(
|
56
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
57
|
-
detail="Database URL is required for initialization",
|
58
|
-
source_function="DatabaseServiceManager.get_instance"
|
59
|
-
)
|
60
|
-
try:
|
61
|
-
cls._instance = DatabaseService(
|
62
|
-
db_url=db_url,
|
63
|
-
db_echo=db_echo,
|
64
|
-
db_pool_size=db_pool_size,
|
65
|
-
db_max_overflow=db_max_overflow,
|
66
|
-
db_pool_timeout=db_pool_timeout,
|
67
|
-
db_pool_recycle=db_pool_recycle,
|
68
|
-
**kwargs
|
69
|
-
)
|
70
|
-
logging.info("Database service initialized successfully")
|
71
|
-
except Exception as e:
|
72
|
-
logging.error(f"Failed to initialize database service: {str(e)}")
|
73
|
-
raise CustomException(
|
74
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
75
|
-
detail=f"Failed to initialize database service: {str(e)}",
|
76
|
-
source_function="DatabaseServiceManager.get_instance",
|
77
|
-
original_error=e
|
78
|
-
)
|
79
|
-
return cls._instance
|
80
|
-
|
81
|
-
@classmethod
|
82
|
-
async def cleanup(cls) -> None:
|
83
|
-
"""데이터베이스 서비스 인스턴스를 정리합니다."""
|
84
|
-
async with cls._lock:
|
85
|
-
if cls._instance:
|
86
|
-
try:
|
87
|
-
if cls._instance.engine:
|
88
|
-
await cls._instance.engine.dispose()
|
89
|
-
cls._instance = None
|
90
|
-
logging.info("Database service cleaned up successfully")
|
91
|
-
except Exception as e:
|
92
|
-
logging.error(f"Error during database service cleanup: {str(e)}")
|
93
|
-
raise CustomException(
|
94
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
95
|
-
detail=f"Failed to cleanup database service: {str(e)}",
|
96
|
-
source_function="DatabaseServiceManager.cleanup",
|
97
|
-
original_error=e
|
98
|
-
)
|
99
|
-
|
100
|
-
@classmethod
|
101
|
-
async def is_initialized(cls) -> bool:
|
102
|
-
"""데이터베이스 서비스가 초기화되었는지 확인합니다."""
|
103
|
-
return cls._instance is not None
|
8
|
+
from .base_model import Base
|
104
9
|
|
105
10
|
class DatabaseService:
|
106
|
-
def __init__(
|
107
|
-
|
108
|
-
db_url: str = None,
|
109
|
-
session: AsyncSession = None,
|
110
|
-
db_echo: bool = False,
|
111
|
-
db_pool_size: int = 5,
|
112
|
-
db_max_overflow: int = 10,
|
113
|
-
db_pool_timeout: int = 30,
|
114
|
-
db_pool_recycle: int = 1800
|
115
|
-
):
|
116
|
-
"""DatabaseService 초기화.
|
11
|
+
def __init__(self, session: AsyncSession):
|
12
|
+
"""DatabaseService 초기화
|
117
13
|
|
118
14
|
Args:
|
119
|
-
|
120
|
-
session (AsyncSession, optional): 기존 세션
|
121
|
-
db_echo (bool, optional): SQL 로깅 여부
|
122
|
-
db_pool_size (int, optional): DB 커넥션 풀 크기
|
123
|
-
db_max_overflow (int, optional): 최대 초과 커넥션 수
|
124
|
-
db_pool_timeout (int, optional): 커넥션 풀 타임아웃
|
125
|
-
db_pool_recycle (int, optional): 커넥션 재활용 시간
|
126
|
-
"""
|
127
|
-
if db_url:
|
128
|
-
self.engine = create_async_engine(
|
129
|
-
db_url,
|
130
|
-
echo=db_echo,
|
131
|
-
pool_size=db_pool_size,
|
132
|
-
max_overflow=db_max_overflow,
|
133
|
-
pool_timeout=db_pool_timeout,
|
134
|
-
pool_recycle=db_pool_recycle,
|
135
|
-
pool_pre_ping=True,
|
136
|
-
poolclass=QueuePool,
|
137
|
-
)
|
138
|
-
self.session_factory = sessionmaker(
|
139
|
-
bind=self.engine,
|
140
|
-
class_=AsyncSession,
|
141
|
-
expire_on_commit=False
|
142
|
-
)
|
143
|
-
self.db = None
|
144
|
-
elif session:
|
145
|
-
self.engine = session.bind
|
146
|
-
self.session_factory = sessionmaker(
|
147
|
-
bind=self.engine,
|
148
|
-
class_=AsyncSession,
|
149
|
-
expire_on_commit=False
|
150
|
-
)
|
151
|
-
self.db = session
|
152
|
-
else:
|
153
|
-
raise CustomException(
|
154
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
155
|
-
detail="Either db_url or session must be provided",
|
156
|
-
source_function="DatabaseService.__init__"
|
157
|
-
)
|
158
|
-
|
159
|
-
async def is_connected(self) -> bool:
|
160
|
-
"""데이터베이스 연결 상태를 확인합니다."""
|
161
|
-
try:
|
162
|
-
if not self.engine:
|
163
|
-
return False
|
164
|
-
async with self.engine.connect() as conn:
|
165
|
-
await conn.execute(select(1))
|
166
|
-
return True
|
167
|
-
except Exception:
|
168
|
-
return False
|
169
|
-
|
170
|
-
async def retry_connection(self, retries: int = 3, delay: int = 2) -> bool:
|
171
|
-
"""데이터베이스 연결을 재시도합니다.
|
172
|
-
|
173
|
-
Args:
|
174
|
-
retries (int): 재시도 횟수
|
175
|
-
delay (int): 재시도 간 대기 시간(초)
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
bool: 연결 성공 여부
|
15
|
+
session (AsyncSession): 외부에서 주입받은 데이터베이스 세션
|
179
16
|
"""
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
logging.error(f"Connection retry attempt {attempt + 1} failed: {str(e)}")
|
187
|
-
if attempt == retries - 1:
|
188
|
-
return False
|
189
|
-
return False
|
190
|
-
|
191
|
-
@asynccontextmanager
|
192
|
-
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
|
193
|
-
"""데이터베이스 세션을 생성하고 반환하는 비동기 컨텍스트 매니저."""
|
194
|
-
if self.session_factory is None:
|
17
|
+
self._session = session
|
18
|
+
|
19
|
+
@property
|
20
|
+
def session(self) -> AsyncSession:
|
21
|
+
"""현재 세션을 반환합니다."""
|
22
|
+
if self._session is None:
|
195
23
|
raise CustomException(
|
196
24
|
ErrorCode.DB_CONNECTION_ERROR,
|
197
|
-
detail="
|
198
|
-
source_function="DatabaseService.
|
25
|
+
detail="session",
|
26
|
+
source_function="DatabaseService.session"
|
199
27
|
)
|
200
|
-
|
201
|
-
async with self.session_factory() as session:
|
202
|
-
try:
|
203
|
-
yield session
|
204
|
-
finally:
|
205
|
-
await session.close()
|
206
|
-
|
207
|
-
def preprocess_data(self, model: Type[Base], input_data: Dict[str, Any], existing_data: Dict[str, Any] = None) -> Dict[str, Any]:
|
208
|
-
"""입력 데이터를 전처리하여 extra_data로 분리
|
209
|
-
|
210
|
-
Args:
|
211
|
-
model (Type[Base]): SQLAlchemy 모델 클래스
|
212
|
-
input_data (Dict[str, Any]): 입력 데이터
|
213
|
-
existing_data (Dict[str, Any], optional): 기존 데이터. Defaults to None.
|
214
|
-
|
215
|
-
Returns:
|
216
|
-
Dict[str, Any]: 전처리된 데이터
|
217
|
-
"""
|
218
|
-
model_attrs = {
|
219
|
-
attr for attr in dir(model)
|
220
|
-
if not attr.startswith('_') and not callable(getattr(model, attr))
|
221
|
-
}
|
222
|
-
model_data = {}
|
223
|
-
extra_data = {}
|
224
|
-
|
225
|
-
# 기존 extra_data가 있으면 복사
|
226
|
-
if existing_data and "extra_data" in existing_data:
|
227
|
-
extra_data = existing_data["extra_data"].copy()
|
28
|
+
return self._session
|
228
29
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
if key in swagger_patterns:
|
236
|
-
continue
|
237
|
-
|
238
|
-
if key in model_attrs:
|
239
|
-
model_data[key] = value
|
240
|
-
else:
|
241
|
-
extra_data[key] = value
|
242
|
-
|
243
|
-
# extra_data가 있고, 모델이 extra_data 속성을 가지고 있으면 추가
|
244
|
-
if extra_data and "extra_data" in model_attrs:
|
245
|
-
model_data["extra_data"] = extra_data
|
246
|
-
|
247
|
-
return model_data
|
248
|
-
|
249
|
-
############################
|
250
|
-
# 2. 트랜잭션 및 세션 관리 #
|
251
|
-
############################
|
252
|
-
@asynccontextmanager
|
253
|
-
async def transaction(self):
|
254
|
-
"""트랜잭션 컨텍스트 매니저
|
255
|
-
|
256
|
-
트랜잭션 범위를 명시적으로 관리합니다.
|
257
|
-
with 문을 벗어날 때 자동으로 commit 또는 rollback됩니다.
|
258
|
-
|
259
|
-
Example:
|
260
|
-
async with db_service.transaction():
|
261
|
-
await db_service.create_entity(...)
|
262
|
-
await db_service.update_entity(...)
|
263
|
-
"""
|
264
|
-
try:
|
265
|
-
yield
|
266
|
-
await self.db.commit()
|
267
|
-
except Exception as e:
|
268
|
-
await self.db.rollback()
|
269
|
-
raise e
|
270
|
-
|
271
|
-
#######################
|
272
|
-
# 3. 데이터 처리 #
|
273
|
-
#######################
|
274
|
-
async def create_entity(self, model: Type[Base], entity_data: Dict[str, Any]) -> Any:
|
275
|
-
"""새로운 엔티티를 생성합니다.
|
30
|
+
async def create_entity(
|
31
|
+
self,
|
32
|
+
model: Type[Base],
|
33
|
+
entity_data: Dict[str, Any]
|
34
|
+
) -> Any:
|
35
|
+
"""엔티티를 생성합니다.
|
276
36
|
|
277
37
|
Args:
|
278
|
-
model
|
279
|
-
entity_data
|
38
|
+
model: 모델 클래스
|
39
|
+
entity_data: 생성할 엔티티 데이터
|
280
40
|
|
281
41
|
Returns:
|
282
|
-
|
42
|
+
생성된 엔티티
|
283
43
|
|
284
44
|
Raises:
|
285
|
-
CustomException:
|
45
|
+
CustomException: 엔티티 생성 실패 시
|
286
46
|
"""
|
287
47
|
try:
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
foreign_key_fields = {
|
293
|
-
field: value for field, value in processed_data.items()
|
294
|
-
if any(fk.parent.name == field for fk in model.__table__.foreign_keys)
|
295
|
-
}
|
296
|
-
if foreign_key_fields:
|
297
|
-
await self.validate_foreign_key_fields(model, foreign_key_fields)
|
298
|
-
|
299
|
-
entity = model(**processed_data)
|
300
|
-
|
301
|
-
self.db.add(entity)
|
302
|
-
await self.db.flush()
|
303
|
-
await self.db.commit()
|
304
|
-
await self.db.refresh(entity)
|
48
|
+
entity = model(**entity_data)
|
49
|
+
self.session.add(entity)
|
50
|
+
await self.session.flush()
|
51
|
+
await self.session.refresh(entity)
|
305
52
|
return entity
|
306
53
|
except IntegrityError as e:
|
307
|
-
await self.
|
308
|
-
error_str = str(e)
|
309
|
-
|
310
|
-
if "duplicate key" in error_str.lower():
|
311
|
-
# 중복 키 에러 처리
|
312
|
-
field = None
|
313
|
-
value = None
|
314
|
-
|
315
|
-
# 에러 메시지에서 필드와 값 추출
|
316
|
-
if "Key (" in error_str and ") already exists" in error_str:
|
317
|
-
field_value = error_str.split("Key (")[1].split(") already exists")[0]
|
318
|
-
if "=" in field_value:
|
319
|
-
field, value = field_value.split("=")
|
320
|
-
field = field.strip("() ")
|
321
|
-
value = value.strip("() ")
|
322
|
-
|
323
|
-
if not field:
|
324
|
-
field = "id" # 기본값
|
325
|
-
value = str(entity_data.get(field, ""))
|
326
|
-
|
327
|
-
raise CustomException(
|
328
|
-
ErrorCode.DUPLICATE_ERROR,
|
329
|
-
detail=f"{model.__tablename__}|{field}|{value}",
|
330
|
-
source_function="DatabaseService.create_entity",
|
331
|
-
original_error=e
|
332
|
-
)
|
333
|
-
elif "violates foreign key constraint" in error_str.lower():
|
334
|
-
# 외래키 위반 에러는 validate_foreign_key_fields에서 이미 처리됨
|
335
|
-
raise CustomException(
|
336
|
-
ErrorCode.FOREIGN_KEY_VIOLATION,
|
337
|
-
detail=error_str,
|
338
|
-
source_function="DatabaseService.create_entity",
|
339
|
-
original_error=e
|
340
|
-
)
|
341
|
-
else:
|
342
|
-
raise CustomException(
|
343
|
-
ErrorCode.DB_CREATE_ERROR,
|
344
|
-
detail=f"Failed to create {model.__name__}: {str(e)}",
|
345
|
-
source_function="DatabaseService.create_entity",
|
346
|
-
original_error=e
|
347
|
-
)
|
348
|
-
except CustomException as e:
|
349
|
-
await self.db.rollback()
|
350
|
-
raise e
|
351
|
-
except Exception as e:
|
352
|
-
await self.db.rollback()
|
54
|
+
await self.session.rollback()
|
353
55
|
raise CustomException(
|
354
|
-
ErrorCode.
|
355
|
-
detail=
|
56
|
+
ErrorCode.DB_INTEGRITY_ERROR,
|
57
|
+
detail=str(e),
|
356
58
|
source_function="DatabaseService.create_entity",
|
357
59
|
original_error=e
|
358
60
|
)
|
359
|
-
|
360
|
-
async def retrieve_entity(
|
361
|
-
self,
|
362
|
-
model: Type[T],
|
363
|
-
conditions: Dict[str, Any],
|
364
|
-
join_options: Optional[Union[Load, List[Load]]] = None
|
365
|
-
) -> Optional[T]:
|
366
|
-
"""조건에 맞는 단일 엔티티를 조회합니다.
|
367
|
-
|
368
|
-
Args:
|
369
|
-
model: 모델 클래스
|
370
|
-
conditions: 조회 조건
|
371
|
-
join_options: SQLAlchemy의 joinedload 옵션 또는 옵션 리스트
|
372
|
-
|
373
|
-
Returns:
|
374
|
-
Optional[T]: 조회된 엔티티 또는 None
|
375
|
-
|
376
|
-
Raises:
|
377
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
378
|
-
"""
|
379
|
-
try:
|
380
|
-
stmt = select(model)
|
381
|
-
|
382
|
-
# Join 옵션 적용
|
383
|
-
if join_options is not None:
|
384
|
-
if isinstance(join_options, list):
|
385
|
-
stmt = stmt.options(*join_options)
|
386
|
-
else:
|
387
|
-
stmt = stmt.options(join_options)
|
388
|
-
|
389
|
-
# 조건 적용
|
390
|
-
for key, value in conditions.items():
|
391
|
-
stmt = stmt.where(getattr(model, key) == value)
|
392
|
-
|
393
|
-
result = await self.execute_query(stmt)
|
394
|
-
return result.unique().scalar_one_or_none()
|
395
|
-
|
396
61
|
except Exception as e:
|
62
|
+
await self.session.rollback()
|
397
63
|
raise CustomException(
|
398
|
-
ErrorCode.
|
64
|
+
ErrorCode.DB_CREATE_ERROR,
|
399
65
|
detail=str(e),
|
400
|
-
source_function="DatabaseService.
|
66
|
+
source_function="DatabaseService.create_entity",
|
401
67
|
original_error=e
|
402
68
|
)
|
403
69
|
|
404
|
-
async def
|
70
|
+
async def get_entity(
|
405
71
|
self,
|
406
72
|
model: Type[Base],
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
73
|
+
filters: Dict[str, Any]
|
74
|
+
) -> Optional[Any]:
|
75
|
+
"""필터 조건으로 엔티티를 조회합니다.
|
76
|
+
|
412
77
|
Args:
|
413
|
-
model
|
414
|
-
|
415
|
-
update_data (Dict[str, Any]): 업데이트할 데이터
|
416
|
-
|
417
|
-
Returns:
|
418
|
-
Optional[Base]: 업데이트된 엔티티
|
419
|
-
|
420
|
-
Raises:
|
421
|
-
CustomException: 데이터베이스 오류 발생
|
422
|
-
"""
|
423
|
-
try:
|
424
|
-
# 엔티티 조회
|
425
|
-
stmt = select(model)
|
426
|
-
for key, value in conditions.items():
|
427
|
-
stmt = stmt.where(getattr(model, key) == value)
|
428
|
-
|
429
|
-
result = await self.db.execute(stmt)
|
430
|
-
entity = result.scalar_one_or_none()
|
431
|
-
|
432
|
-
if not entity:
|
433
|
-
return None
|
434
|
-
|
435
|
-
# 기존 데이터를 딕셔너리로 변환
|
436
|
-
existing_data = {
|
437
|
-
column.name: getattr(entity, column.name)
|
438
|
-
for column in entity.__table__.columns
|
439
|
-
}
|
440
|
-
|
441
|
-
# 데이터 전처리
|
442
|
-
processed_data = self.preprocess_data(model, update_data, existing_data)
|
443
|
-
|
444
|
-
# UPDATE 문 생성 및 실행
|
445
|
-
update_stmt = (
|
446
|
-
update(model)
|
447
|
-
.where(and_(*[getattr(model, key) == value for key, value in conditions.items()]))
|
448
|
-
.values(**processed_data)
|
449
|
-
.returning(model)
|
450
|
-
)
|
451
|
-
|
452
|
-
result = await self.db.execute(update_stmt)
|
453
|
-
await self.db.commit()
|
454
|
-
|
455
|
-
# 업데이트된 엔티티 반환
|
456
|
-
updated_entity = result.scalar_one()
|
457
|
-
return updated_entity
|
78
|
+
model: 모델 클래스
|
79
|
+
filters: 필터 조건
|
458
80
|
|
459
|
-
except SQLAlchemyError as e:
|
460
|
-
await self.db.rollback()
|
461
|
-
raise CustomException(
|
462
|
-
ErrorCode.DB_UPDATE_ERROR,
|
463
|
-
detail=f"Failed to update {model.__name__}: {str(e)}",
|
464
|
-
source_function="DatabaseService.update_entity",
|
465
|
-
original_error=e
|
466
|
-
)
|
467
|
-
except Exception as e:
|
468
|
-
await self.db.rollback()
|
469
|
-
raise CustomException(
|
470
|
-
ErrorCode.UNEXPECTED_ERROR,
|
471
|
-
detail=f"Unexpected error while updating {model.__name__}: {str(e)}",
|
472
|
-
source_function="DatabaseService.update_entity",
|
473
|
-
original_error=e
|
474
|
-
)
|
475
|
-
|
476
|
-
async def delete_entity(self, entity: T) -> None:
|
477
|
-
"""엔티티를 실제로 삭제합니다.
|
478
|
-
|
479
|
-
Args:
|
480
|
-
entity (T): 삭제할 엔티티
|
481
|
-
|
482
|
-
Raises:
|
483
|
-
CustomException: 데이터베이스 오류 발생
|
484
|
-
"""
|
485
|
-
try:
|
486
|
-
await self.db.delete(entity)
|
487
|
-
await self.db.flush()
|
488
|
-
await self.db.commit()
|
489
|
-
except SQLAlchemyError as e:
|
490
|
-
await self.db.rollback()
|
491
|
-
raise CustomException(
|
492
|
-
ErrorCode.DB_DELETE_ERROR,
|
493
|
-
detail=f"Failed to delete entity: {str(e)}",
|
494
|
-
source_function="DatabaseService.delete_entity",
|
495
|
-
original_error=e
|
496
|
-
)
|
497
|
-
except Exception as e:
|
498
|
-
await self.db.rollback()
|
499
|
-
raise CustomException(
|
500
|
-
ErrorCode.UNEXPECTED_ERROR,
|
501
|
-
detail=f"Unexpected error while deleting entity: {str(e)}",
|
502
|
-
source_function="DatabaseService.delete_entity",
|
503
|
-
original_error=e
|
504
|
-
)
|
505
|
-
|
506
|
-
async def soft_delete_entity(self, model: Type[T], ulid: str) -> Optional[T]:
|
507
|
-
"""엔티티를 소프트 삭제합니다 (is_deleted = True).
|
508
|
-
|
509
|
-
Args:
|
510
|
-
model (Type[T]): 엔티티 모델
|
511
|
-
ulid (str): 삭제할 엔티티의 ULID
|
512
|
-
|
513
81
|
Returns:
|
514
|
-
|
515
|
-
|
82
|
+
조회된 엔티티 또는 None
|
83
|
+
|
516
84
|
Raises:
|
517
|
-
CustomException:
|
85
|
+
CustomException: 조회 실패 시
|
518
86
|
"""
|
519
87
|
try:
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
model.ulid == ulid,
|
524
|
-
model.is_deleted == False
|
525
|
-
)
|
526
|
-
)
|
527
|
-
result = await self.db.execute(stmt)
|
528
|
-
entity = result.scalar_one_or_none()
|
529
|
-
|
530
|
-
if not entity:
|
531
|
-
return None
|
532
|
-
|
533
|
-
# 2. 소프트 삭제 처리
|
534
|
-
stmt = update(model).where(
|
535
|
-
model.ulid == ulid
|
536
|
-
).values(
|
537
|
-
is_deleted=True
|
538
|
-
)
|
539
|
-
await self.db.execute(stmt)
|
540
|
-
await self.db.commit()
|
541
|
-
|
542
|
-
# 3. 업데이트된 엔티티 반환
|
543
|
-
return entity
|
544
|
-
except SQLAlchemyError as e:
|
545
|
-
await self.db.rollback()
|
546
|
-
raise CustomException(
|
547
|
-
ErrorCode.DB_DELETE_ERROR,
|
548
|
-
detail=f"Failed to soft delete {model.__name__}: {str(e)}",
|
549
|
-
source_function="DatabaseService.soft_delete_entity",
|
550
|
-
original_error=e
|
551
|
-
)
|
88
|
+
stmt = select(model).filter_by(**filters)
|
89
|
+
result = await self.session.execute(stmt)
|
90
|
+
return result.scalars().first()
|
552
91
|
except Exception as e:
|
553
|
-
await self.db.rollback()
|
554
92
|
raise CustomException(
|
555
|
-
ErrorCode.
|
556
|
-
detail=
|
557
|
-
source_function="DatabaseService.
|
93
|
+
ErrorCode.DB_QUERY_ERROR,
|
94
|
+
detail=str(e),
|
95
|
+
source_function="DatabaseService.get_entity",
|
558
96
|
original_error=e
|
559
97
|
)
|
560
98
|
|
561
99
|
async def list_entities(
|
562
100
|
self,
|
563
|
-
model: Type[
|
564
|
-
skip: int = 0,
|
565
|
-
limit: int = 100,
|
101
|
+
model: Type[Base],
|
566
102
|
filters: Optional[Dict[str, Any]] = None,
|
567
|
-
|
568
|
-
|
103
|
+
skip: int = 0,
|
104
|
+
limit: int = 100
|
105
|
+
) -> List[Any]:
|
569
106
|
"""엔티티 목록을 조회합니다.
|
570
107
|
|
571
108
|
Args:
|
572
|
-
model
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
- field: value -> field = value
|
577
|
-
- field__ilike: value -> field ILIKE value
|
578
|
-
- search: [(field, pattern), ...] -> OR(field ILIKE pattern, ...)
|
579
|
-
joins (Optional[List[Any]]): 조인할 관계들 (joinedload 객체 리스트)
|
109
|
+
model: 모델 클래스
|
110
|
+
filters: 필터 조건
|
111
|
+
skip: 건너뛸 레코드 수
|
112
|
+
limit: 조회할 최대 레코드 수
|
580
113
|
|
581
114
|
Returns:
|
582
|
-
|
115
|
+
엔티티 목록
|
583
116
|
|
584
117
|
Raises:
|
585
|
-
CustomException:
|
118
|
+
CustomException: 조회 실패 시
|
586
119
|
"""
|
587
120
|
try:
|
588
|
-
|
589
|
-
conditions = []
|
590
|
-
|
121
|
+
stmt = select(model)
|
591
122
|
if filters:
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
for field_name, pattern in value:
|
597
|
-
field = getattr(model, field_name)
|
598
|
-
search_conditions.append(field.ilike(pattern))
|
599
|
-
if search_conditions:
|
600
|
-
conditions.append(or_(*search_conditions))
|
601
|
-
elif "__ilike" in key:
|
602
|
-
# ILIKE 검색
|
603
|
-
field_name = key.replace("__ilike", "")
|
604
|
-
field = getattr(model, field_name)
|
605
|
-
conditions.append(field.ilike(value))
|
606
|
-
else:
|
607
|
-
# 일반 필터
|
608
|
-
field = getattr(model, key)
|
609
|
-
conditions.append(field == value)
|
610
|
-
|
611
|
-
if conditions:
|
612
|
-
query = query.where(and_(*conditions))
|
613
|
-
|
614
|
-
if joins:
|
615
|
-
for join_option in joins:
|
616
|
-
query = query.options(join_option)
|
617
|
-
|
618
|
-
query = query.offset(skip).limit(limit)
|
619
|
-
result = await self.db.execute(query)
|
620
|
-
return result.scalars().unique().all()
|
621
|
-
except SQLAlchemyError as e:
|
622
|
-
raise CustomException(
|
623
|
-
ErrorCode.DB_READ_ERROR,
|
624
|
-
detail=f"Failed to list {model.__name__}: {str(e)}",
|
625
|
-
source_function="DatabaseService.list_entities",
|
626
|
-
original_error=e
|
627
|
-
)
|
628
|
-
except Exception as e:
|
629
|
-
raise CustomException(
|
630
|
-
ErrorCode.UNEXPECTED_ERROR,
|
631
|
-
detail=f"Unexpected error while listing {model.__name__}: {str(e)}",
|
632
|
-
source_function="DatabaseService.list_entities",
|
633
|
-
original_error=e
|
634
|
-
)
|
635
|
-
|
636
|
-
######################
|
637
|
-
# 4. 검증 #
|
638
|
-
######################
|
639
|
-
async def validate_unique_fields(
|
640
|
-
self,
|
641
|
-
table_or_model: Union[Table, Type[Any]],
|
642
|
-
fields: Dict[str, Any],
|
643
|
-
source_function: str,
|
644
|
-
error_code: ErrorCode = ErrorCode.DUPLICATE_ERROR
|
645
|
-
) -> None:
|
646
|
-
"""
|
647
|
-
데이터베이스에서 필드의 유일성을 검증합니다.
|
648
|
-
|
649
|
-
Args:
|
650
|
-
table_or_model: 검증할 테이블 또는 모델 클래스
|
651
|
-
fields: 검증할 필드와 값의 딕셔너리 {"field_name": value}
|
652
|
-
source_function: 호출한 함수명
|
653
|
-
error_code: 사용할 에러 코드 (기본값: DUPLICATE_ERROR)
|
654
|
-
|
655
|
-
Raises:
|
656
|
-
CustomException: 중복된 값이 존재할 경우
|
657
|
-
"""
|
658
|
-
try:
|
659
|
-
conditions = []
|
660
|
-
for field_name, value in fields.items():
|
661
|
-
conditions.append(getattr(table_or_model, field_name) == value)
|
662
|
-
|
663
|
-
query = select(table_or_model).where(or_(*conditions))
|
664
|
-
result = await self.db.execute(query)
|
665
|
-
existing = result.scalar_one_or_none()
|
666
|
-
|
667
|
-
if existing:
|
668
|
-
table_name = table_or_model.name if hasattr(table_or_model, 'name') else table_or_model.__tablename__
|
669
|
-
# 단일 필드인 경우
|
670
|
-
if len(fields) == 1:
|
671
|
-
field_name, value = next(iter(fields.items()))
|
672
|
-
detail = f"{table_name}|{field_name}|{value}"
|
673
|
-
# 복수 필드인 경우
|
674
|
-
else:
|
675
|
-
fields_str = "|".join(f"{k}:{v}" for k, v in fields.items())
|
676
|
-
detail = f"{table_name}|{fields_str}"
|
677
|
-
|
678
|
-
raise CustomException(
|
679
|
-
error_code,
|
680
|
-
detail=detail,
|
681
|
-
source_function="DatabaseService.validate_unique_fields"
|
682
|
-
)
|
683
|
-
|
684
|
-
except CustomException as e:
|
685
|
-
raise CustomException(
|
686
|
-
e.error_code,
|
687
|
-
detail=e.detail,
|
688
|
-
source_function="DatabaseService.validate_unique_fields",
|
689
|
-
original_error=e.original_error,
|
690
|
-
parent_source_function=e.source_function
|
691
|
-
)
|
123
|
+
stmt = stmt.filter_by(**filters)
|
124
|
+
stmt = stmt.offset(skip).limit(limit)
|
125
|
+
result = await self.session.execute(stmt)
|
126
|
+
return result.scalars().all()
|
692
127
|
except Exception as e:
|
693
128
|
raise CustomException(
|
694
129
|
ErrorCode.DB_QUERY_ERROR,
|
695
130
|
detail=str(e),
|
696
|
-
source_function="DatabaseService.
|
131
|
+
source_function="DatabaseService.list_entities",
|
697
132
|
original_error=e
|
698
133
|
)
|
699
134
|
|
700
|
-
async def
|
135
|
+
async def update_entity(
|
701
136
|
self,
|
702
|
-
|
703
|
-
|
704
|
-
) ->
|
705
|
-
"""
|
137
|
+
entity: Base,
|
138
|
+
update_data: Dict[str, Any]
|
139
|
+
) -> Any:
|
140
|
+
"""엔티티를 수정합니다.
|
706
141
|
|
707
142
|
Args:
|
708
|
-
|
709
|
-
|
143
|
+
entity: 수정할 엔티티
|
144
|
+
update_data: 수정할 데이터
|
710
145
|
|
711
|
-
Raises:
|
712
|
-
CustomException: 참조하는 레코드가 존재하지 않는 경우
|
713
|
-
"""
|
714
|
-
for field, value in fields.items():
|
715
|
-
# 외래 키 관계 정보 가져오기
|
716
|
-
foreign_key = next(
|
717
|
-
(fk for fk in model.__table__.foreign_keys if fk.parent.name == field),
|
718
|
-
None
|
719
|
-
)
|
720
|
-
if foreign_key and value:
|
721
|
-
# 참조하는 테이블에서 레코드 존재 여부 확인
|
722
|
-
referenced_table = foreign_key.column.table
|
723
|
-
query = select(referenced_table).where(
|
724
|
-
and_(
|
725
|
-
foreign_key.column == value,
|
726
|
-
getattr(referenced_table.c, 'is_deleted', None) == False
|
727
|
-
)
|
728
|
-
)
|
729
|
-
result = await self.db.execute(query)
|
730
|
-
if not result.scalar_one_or_none():
|
731
|
-
raise CustomException(
|
732
|
-
ErrorCode.FOREIGN_KEY_VIOLATION,
|
733
|
-
detail=f"{referenced_table.name}|{field}|{value}",
|
734
|
-
source_function="DatabaseService.validate_foreign_key_fields"
|
735
|
-
)
|
736
|
-
|
737
|
-
#######################
|
738
|
-
# 5. 쿼리 실행 #
|
739
|
-
#######################
|
740
|
-
async def create_log(self, model: Type[Base], log_data: Dict[str, Any], request: Request = None) -> None:
|
741
|
-
"""로그를 생성합니다.
|
742
|
-
|
743
|
-
Args:
|
744
|
-
model: 로그 모델 클래스
|
745
|
-
log_data: 로그 데이터
|
746
|
-
request: FastAPI 요청 객체
|
747
|
-
|
748
146
|
Returns:
|
749
|
-
|
750
|
-
|
751
|
-
Raises:
|
752
|
-
CustomException: 로그 생성 실패 시
|
753
|
-
"""
|
754
|
-
try:
|
755
|
-
# 공통 필드 추가 (ULID를 문자열로 변환)
|
756
|
-
log_data["ulid"] = str(ULID())
|
757
|
-
|
758
|
-
# request가 있는 경우 user-agent와 ip 정보 추가
|
759
|
-
if request:
|
760
|
-
log_data["user_agent"] = request.headers.get("user-agent")
|
761
|
-
log_data["ip_address"] = request.headers.get("x-forwarded-for")
|
762
|
-
|
763
|
-
# 데이터 전처리
|
764
|
-
processed_data = self.preprocess_data(model, log_data)
|
765
|
-
entity = model(**processed_data)
|
766
|
-
|
767
|
-
async with self.get_session() as session:
|
768
|
-
# 로그 엔티티 저장
|
769
|
-
session.add(entity)
|
770
|
-
await session.flush()
|
771
|
-
await session.commit()
|
772
|
-
return entity
|
773
|
-
|
774
|
-
except Exception as e:
|
775
|
-
logging.error(f"Failed to create log: {str(e)}")
|
776
|
-
# 로그 생성 실패는 원래 작업에 영향을 주지 않도록 함
|
777
|
-
return None
|
778
|
-
|
779
|
-
async def soft_delete(
|
780
|
-
self,
|
781
|
-
model: Type[T],
|
782
|
-
entity_id: str,
|
783
|
-
source_function: str = None,
|
784
|
-
request: Request = None
|
785
|
-
) -> None:
|
786
|
-
"""엔티티를 소프트 삭제합니다.
|
787
|
-
|
788
|
-
Args:
|
789
|
-
model: 모델 클래스
|
790
|
-
entity_id: 삭제할 엔티티의 ID
|
791
|
-
source_function: 호출한 함수명
|
792
|
-
request: FastAPI 요청 객체
|
147
|
+
수정된 엔티티
|
793
148
|
|
794
149
|
Raises:
|
795
|
-
CustomException:
|
150
|
+
CustomException: 수정 실패 시
|
796
151
|
"""
|
797
152
|
try:
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
)
|
805
|
-
result = await self.db.execute(stmt)
|
806
|
-
entity = result.scalar_one_or_none()
|
807
|
-
|
808
|
-
if not entity:
|
809
|
-
raise CustomException(
|
810
|
-
ErrorCode.NOT_FOUND,
|
811
|
-
detail=f"{model.__name__}|{entity_id}",
|
812
|
-
source_function=source_function or "DatabaseService.soft_delete"
|
813
|
-
)
|
814
|
-
|
815
|
-
# 2. 소프트 삭제 처리
|
816
|
-
stmt = update(model).where(
|
817
|
-
model.ulid == entity_id
|
818
|
-
).values(
|
819
|
-
is_deleted=True
|
820
|
-
)
|
821
|
-
await self.db.execute(stmt)
|
822
|
-
await self.db.commit()
|
823
|
-
|
824
|
-
# 3. 삭제 로그 생성
|
825
|
-
if request:
|
826
|
-
activity_type = f"{model.__tablename__.upper()}_DELETED"
|
827
|
-
await self.create_log({
|
828
|
-
"type": activity_type,
|
829
|
-
"fk_table": model.__tablename__,
|
830
|
-
"extra_data": {
|
831
|
-
f"{model.__tablename__}_ulid": entity_id
|
832
|
-
}
|
833
|
-
}, request)
|
834
|
-
|
835
|
-
except CustomException as e:
|
836
|
-
await self.db.rollback()
|
837
|
-
raise CustomException(
|
838
|
-
e.error_code,
|
839
|
-
detail=e.detail,
|
840
|
-
source_function=source_function or "DatabaseService.soft_delete",
|
841
|
-
original_error=e.original_error,
|
842
|
-
parent_source_function=e.source_function
|
843
|
-
)
|
844
|
-
except Exception as e:
|
845
|
-
await self.db.rollback()
|
153
|
+
for key, value in update_data.items():
|
154
|
+
setattr(entity, key, value)
|
155
|
+
await self.session.flush()
|
156
|
+
await self.session.refresh(entity)
|
157
|
+
return entity
|
158
|
+
except IntegrityError as e:
|
159
|
+
await self.session.rollback()
|
846
160
|
raise CustomException(
|
847
|
-
ErrorCode.
|
161
|
+
ErrorCode.DB_INTEGRITY_ERROR,
|
848
162
|
detail=str(e),
|
849
|
-
source_function=
|
850
|
-
original_error=e
|
851
|
-
)
|
852
|
-
|
853
|
-
async def get_entity(self, model: Type[T], ulid: str) -> Optional[T]:
|
854
|
-
"""ULID로 엔티티를 조회합니다.
|
855
|
-
|
856
|
-
Args:
|
857
|
-
model (Type[T]): 엔티티 모델
|
858
|
-
ulid (str): 조회할 엔티티의 ULID
|
859
|
-
|
860
|
-
Returns:
|
861
|
-
Optional[T]: 조회된 엔티티 또는 None
|
862
|
-
|
863
|
-
Raises:
|
864
|
-
CustomException: 데이터베이스 오류 발생
|
865
|
-
"""
|
866
|
-
try:
|
867
|
-
query = select(model).where(
|
868
|
-
and_(
|
869
|
-
model.ulid == ulid,
|
870
|
-
model.is_deleted == False
|
871
|
-
)
|
872
|
-
)
|
873
|
-
result = await self.db.execute(query)
|
874
|
-
return result.scalar_one_or_none()
|
875
|
-
except SQLAlchemyError as e:
|
876
|
-
raise CustomException(
|
877
|
-
ErrorCode.DB_READ_ERROR,
|
878
|
-
detail=f"Failed to get {model.__name__}: {str(e)}",
|
879
|
-
source_function="DatabaseService.get_entity",
|
163
|
+
source_function="DatabaseService.update_entity",
|
880
164
|
original_error=e
|
881
165
|
)
|
882
166
|
except Exception as e:
|
167
|
+
await self.session.rollback()
|
883
168
|
raise CustomException(
|
884
|
-
ErrorCode.
|
885
|
-
detail=f"Unexpected error while getting {model.__name__}: {str(e)}",
|
886
|
-
source_function="DatabaseService.get_entity",
|
887
|
-
original_error=e
|
888
|
-
)
|
889
|
-
|
890
|
-
async def execute_query(self, query: Select) -> Any:
|
891
|
-
"""SQL 쿼리를 실행하고 결과를 반환합니다.
|
892
|
-
|
893
|
-
Args:
|
894
|
-
query (Select): 실행할 SQLAlchemy 쿼리
|
895
|
-
|
896
|
-
Returns:
|
897
|
-
Any: 쿼리 실행 결과
|
898
|
-
|
899
|
-
Raises:
|
900
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
901
|
-
"""
|
902
|
-
try:
|
903
|
-
if self.db is not None:
|
904
|
-
return await self.db.execute(query)
|
905
|
-
|
906
|
-
async with self.get_session() as session:
|
907
|
-
result = await session.execute(query)
|
908
|
-
return result
|
909
|
-
except Exception as e:
|
910
|
-
raise CustomException(
|
911
|
-
ErrorCode.DB_QUERY_ERROR,
|
169
|
+
ErrorCode.DB_UPDATE_ERROR,
|
912
170
|
detail=str(e),
|
913
|
-
source_function=
|
171
|
+
source_function="DatabaseService.update_entity",
|
914
172
|
original_error=e
|
915
173
|
)
|
916
174
|
|
917
|
-
async def
|
918
|
-
|
175
|
+
async def delete_entity(
|
176
|
+
self,
|
177
|
+
entity: Base,
|
178
|
+
soft_delete: bool = True
|
179
|
+
) -> bool:
|
180
|
+
"""엔티티를 삭제합니다.
|
919
181
|
|
920
182
|
Args:
|
921
|
-
|
183
|
+
entity: 삭제할 엔티티
|
184
|
+
soft_delete: 소프트 삭제 여부
|
922
185
|
|
923
186
|
Returns:
|
924
|
-
|
187
|
+
삭제 성공 여부
|
925
188
|
|
926
189
|
Raises:
|
927
|
-
CustomException:
|
190
|
+
CustomException: 삭제 실패 시
|
928
191
|
"""
|
929
192
|
try:
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
)
|
193
|
+
if soft_delete:
|
194
|
+
entity.is_deleted = True
|
195
|
+
await self.session.flush()
|
196
|
+
else:
|
197
|
+
await self.session.delete(entity)
|
198
|
+
await self.session.flush()
|
199
|
+
return True
|
938
200
|
except Exception as e:
|
201
|
+
await self.session.rollback()
|
939
202
|
raise CustomException(
|
940
|
-
ErrorCode.
|
941
|
-
detail=str(e),
|
942
|
-
source_function="DatabaseService.execute",
|
943
|
-
original_error=e
|
944
|
-
)
|
945
|
-
|
946
|
-
async def commit(self) -> None:
|
947
|
-
"""현재 세션의 변경사항을 데이터베이스에 커밋합니다."""
|
948
|
-
try:
|
949
|
-
await self.db.commit()
|
950
|
-
except SQLAlchemyError as e:
|
951
|
-
await self.db.rollback()
|
952
|
-
raise CustomException(
|
953
|
-
ErrorCode.DB_QUERY_ERROR,
|
954
|
-
detail=str(e),
|
955
|
-
source_function="DatabaseService.commit",
|
956
|
-
original_error=e
|
957
|
-
)
|
958
|
-
|
959
|
-
async def rollback(self) -> None:
|
960
|
-
"""현재 세션의 변경사항을 롤백합니다."""
|
961
|
-
try:
|
962
|
-
await self.db.rollback()
|
963
|
-
except SQLAlchemyError as e:
|
964
|
-
raise CustomException(
|
965
|
-
ErrorCode.DB_QUERY_ERROR,
|
966
|
-
detail=str(e),
|
967
|
-
source_function="DatabaseService.rollback",
|
968
|
-
original_error=e
|
969
|
-
)
|
970
|
-
|
971
|
-
async def flush(self) -> None:
|
972
|
-
"""현재 세션의 변경사항을 데이터베이스에 플러시합니다."""
|
973
|
-
try:
|
974
|
-
await self.db.flush()
|
975
|
-
except SQLAlchemyError as e:
|
976
|
-
await self.db.rollback()
|
977
|
-
raise CustomException(
|
978
|
-
ErrorCode.DB_QUERY_ERROR,
|
979
|
-
detail=str(e),
|
980
|
-
source_function="DatabaseService.flush",
|
981
|
-
original_error=e
|
982
|
-
)
|
983
|
-
|
984
|
-
async def refresh(self, entity: Any) -> None:
|
985
|
-
"""엔티티를 데이터베이스의 최신 상태로 리프레시합니다.
|
986
|
-
|
987
|
-
Args:
|
988
|
-
entity: 리프레시할 엔티티
|
989
|
-
"""
|
990
|
-
try:
|
991
|
-
await self.db.refresh(entity)
|
992
|
-
except SQLAlchemyError as e:
|
993
|
-
raise CustomException(
|
994
|
-
ErrorCode.DB_QUERY_ERROR,
|
203
|
+
ErrorCode.DB_DELETE_ERROR,
|
995
204
|
detail=str(e),
|
996
|
-
source_function="DatabaseService.
|
997
|
-
original_error=e
|
998
|
-
)
|
999
|
-
|
1000
|
-
async def create_session(self) -> AsyncSession:
|
1001
|
-
"""새로운 데이터베이스 세션을 생성합니다.
|
1002
|
-
|
1003
|
-
Returns:
|
1004
|
-
AsyncSession: 생성된 세션
|
1005
|
-
|
1006
|
-
Raises:
|
1007
|
-
CustomException: 세션 생성 실패 시
|
1008
|
-
"""
|
1009
|
-
if self.session_factory is None:
|
1010
|
-
raise CustomException(
|
1011
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
1012
|
-
detail="session_factory is not initialized",
|
1013
|
-
source_function="DatabaseService.create_session"
|
1014
|
-
)
|
1015
|
-
|
1016
|
-
return self.session_factory()
|
1017
|
-
|
1018
|
-
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
1019
|
-
"""데이터베이스 세션 의존성
|
1020
|
-
|
1021
|
-
Yields:
|
1022
|
-
AsyncSession: 데이터베이스 세션
|
1023
|
-
|
1024
|
-
Raises:
|
1025
|
-
CustomException: 데이터베이스 연결 오류
|
1026
|
-
"""
|
1027
|
-
db_service = await get_database_service()
|
1028
|
-
|
1029
|
-
async with db_service.get_session() as session:
|
1030
|
-
try:
|
1031
|
-
# 세션이 유효한지 확인
|
1032
|
-
await session.execute(select(1))
|
1033
|
-
yield session
|
1034
|
-
except Exception as e:
|
1035
|
-
if isinstance(e, CustomException):
|
1036
|
-
raise e
|
1037
|
-
raise CustomException(
|
1038
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
1039
|
-
detail=f"Failed to get database session: {str(e)}",
|
1040
|
-
source_function="get_db",
|
205
|
+
source_function="DatabaseService.delete_entity",
|
1041
206
|
original_error=e
|
1042
|
-
)
|
1043
|
-
|
1044
|
-
async def get_database_service() -> DatabaseService:
|
1045
|
-
"""DatabaseService 의존성
|
1046
|
-
|
1047
|
-
Returns:
|
1048
|
-
DatabaseService: DatabaseService 인스턴스
|
1049
|
-
|
1050
|
-
Raises:
|
1051
|
-
CustomException: 데이터베이스 서비스가 초기화되지 않은 경우
|
1052
|
-
"""
|
1053
|
-
if not await DatabaseServiceManager.is_initialized():
|
1054
|
-
raise CustomException(
|
1055
|
-
ErrorCode.DB_CONNECTION_ERROR,
|
1056
|
-
detail="Database service is not initialized",
|
1057
|
-
source_function="get_database_service"
|
1058
|
-
)
|
1059
|
-
|
1060
|
-
return await DatabaseServiceManager.get_instance()
|
1061
|
-
|
1062
|
-
@asynccontextmanager
|
1063
|
-
async def lifespan(app: FastAPI):
|
1064
|
-
"""FastAPI 애플리케이션 라이프사이클 관리자.
|
1065
|
-
|
1066
|
-
Args:
|
1067
|
-
app (FastAPI): FastAPI 애플리케이션 인스턴스
|
1068
|
-
"""
|
1069
|
-
try:
|
1070
|
-
# 시작 시 초기화
|
1071
|
-
if not await DatabaseServiceManager.is_initialized():
|
1072
|
-
await DatabaseServiceManager.get_instance(
|
1073
|
-
db_url=app.state.settings.DATABASE_URL,
|
1074
|
-
db_echo=app.state.settings.DB_ECHO,
|
1075
|
-
db_pool_size=app.state.settings.DB_POOL_SIZE,
|
1076
|
-
db_max_overflow=app.state.settings.DB_MAX_OVERFLOW,
|
1077
|
-
db_pool_timeout=app.state.settings.DB_POOL_TIMEOUT,
|
1078
|
-
db_pool_recycle=app.state.settings.DB_POOL_RECYCLE
|
1079
|
-
)
|
1080
|
-
yield
|
1081
|
-
finally:
|
1082
|
-
# 종료 시 정리
|
1083
|
-
await DatabaseServiceManager.cleanup()
|
207
|
+
)
|
aiteamutils/dependencies.py
CHANGED
@@ -9,33 +9,13 @@ from .exceptions import CustomException, ErrorCode
|
|
9
9
|
from .config import get_settings
|
10
10
|
from .base_service import BaseService
|
11
11
|
from .base_repository import BaseRepository
|
12
|
-
from .database import
|
12
|
+
from .database import DatabaseService
|
13
13
|
|
14
14
|
T = TypeVar("T", bound=BaseService)
|
15
15
|
R = TypeVar("R", bound=BaseRepository)
|
16
16
|
|
17
17
|
_service_registry: Dict[str, Dict[str, Any]] = {}
|
18
18
|
|
19
|
-
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
20
|
-
"""데이터베이스 세션을 반환합니다.
|
21
|
-
|
22
|
-
Yields:
|
23
|
-
AsyncSession: 데이터베이스 세션
|
24
|
-
|
25
|
-
Raises:
|
26
|
-
CustomException: 세션 생성 실패 시
|
27
|
-
"""
|
28
|
-
try:
|
29
|
-
async with db_manager.get_session() as session:
|
30
|
-
yield session
|
31
|
-
except Exception as e:
|
32
|
-
raise CustomException(
|
33
|
-
ErrorCode.DATABASE_ERROR,
|
34
|
-
detail=str(e),
|
35
|
-
source_function="dependencies.get_db",
|
36
|
-
original_error=e
|
37
|
-
)
|
38
|
-
|
39
19
|
def register_service(
|
40
20
|
service_class: Type[T],
|
41
21
|
repository_class: Optional[Type[R]] = None,
|
@@ -86,6 +66,9 @@ async def _get_service(
|
|
86
66
|
repository_class = service_info["repository_class"]
|
87
67
|
dependencies = service_info["dependencies"]
|
88
68
|
|
69
|
+
# 데이터베이스 서비스 생성
|
70
|
+
db_service = DatabaseService(session=session)
|
71
|
+
|
89
72
|
# 저장소 인스턴스 생성
|
90
73
|
repository = None
|
91
74
|
if repository_class:
|
@@ -93,8 +76,8 @@ async def _get_service(
|
|
93
76
|
|
94
77
|
# 서비스 인스턴스 생성
|
95
78
|
service = service_class(
|
79
|
+
db=db_service,
|
96
80
|
repository=repository,
|
97
|
-
session=session,
|
98
81
|
request=request,
|
99
82
|
**dependencies
|
100
83
|
)
|
@@ -122,14 +105,14 @@ def get_service(service_name: str) -> Callable:
|
|
122
105
|
"""
|
123
106
|
async def _get_service_dependency(
|
124
107
|
request: Request,
|
125
|
-
session: AsyncSession
|
108
|
+
session: AsyncSession
|
126
109
|
) -> BaseService:
|
127
110
|
return await _get_service(service_name, session, request)
|
128
111
|
return _get_service_dependency
|
129
112
|
|
130
113
|
async def get_current_user(
|
131
114
|
request: Request,
|
132
|
-
session: AsyncSession
|
115
|
+
session: AsyncSession,
|
133
116
|
auth_service: BaseService = Depends(get_service("AuthService"))
|
134
117
|
) -> Dict[str, Any]:
|
135
118
|
"""현재 사용자 정보를 반환합니다.
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.55"
|
@@ -1,16 +1,16 @@
|
|
1
|
-
aiteamutils/__init__.py,sha256=
|
1
|
+
aiteamutils/__init__.py,sha256=h7iFifWvRlaComsk4VPapRr16YhD19Kp1LuBzLHqdgQ,1170
|
2
2
|
aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
|
3
3
|
aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-U,7846
|
4
4
|
aiteamutils/base_service.py,sha256=s2AcA-6_ogOQKgt2xf_3AG2s6tqBceU4nJoXO1II7S8,24588
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=OM_b7g8sqZ3zY_DSF9ry-zn5wn4dlXdx5OhjfTGr0TE,2876
|
7
|
-
aiteamutils/database.py,sha256=
|
8
|
-
aiteamutils/dependencies.py,sha256=
|
7
|
+
aiteamutils/database.py,sha256=x0x5gnSyGfwo_klL9O65RnGOQID6c9tH2miwFveVyoE,6326
|
8
|
+
aiteamutils/dependencies.py,sha256=UrPjMfUlmG1uGGeWI0H7fINfxChL-LTfD-odFUA5xqQ,5965
|
9
9
|
aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
|
10
10
|
aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
|
11
11
|
aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
|
12
12
|
aiteamutils/validators.py,sha256=3N245cZFjgwtW_KzjESkizx5BBUDaJLbbxfNO4WOFZ0,7764
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=q4mITsP_7Bmz6MAA1ME4HFQVgT80ojkzg8h-5uUxde4,42
|
14
|
+
aiteamutils-0.2.55.dist-info/METADATA,sha256=UrrWJHMtqDp8HsWEk3hfzMkKcfm11VbgJv21yorOR1s,1718
|
15
|
+
aiteamutils-0.2.55.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.55.dist-info/RECORD,,
|
File without changes
|