aiteamutils 0.2.53__py3-none-any.whl → 0.2.54__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils/database.py +111 -987
- aiteamutils/dependencies.py +7 -24
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.54.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.54.dist-info}/RECORD +6 -6
- {aiteamutils-0.2.53.dist-info → aiteamutils-0.2.54.dist-info}/WHEEL +0 -0
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.54"
|
@@ -4,13 +4,13 @@ aiteamutils/base_repository.py,sha256=vqsundoN0h7FVvgqTBEnnJNMcFpvMK0s_nxBWdIYg-
|
|
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=JYxMqBzmgoiMWtDBCvBWs8eM_bzABcUkIdy_n1MOyd4,42
|
14
|
+
aiteamutils-0.2.54.dist-info/METADATA,sha256=qbHv86w3VnIJnNHPoUzRRHXcFr47eXOgf9vIFBrBLrY,1718
|
15
|
+
aiteamutils-0.2.54.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.54.dist-info/RECORD,,
|
File without changes
|