aiteamutils 0.2.52__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 CHANGED
@@ -1,1083 +1,207 @@
1
- import asyncio
2
- import logging
3
- from typing import Any, Dict, Optional, Type, AsyncGenerator, TypeVar, List, Union
4
- from sqlalchemy import select, update, and_, Table
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, BaseColumn
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
- self,
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
- db_url (str, optional): 데이터베이스 URL
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
- for attempt in range(retries):
181
- try:
182
- if await self.is_connected():
183
- return True
184
- await asyncio.sleep(delay)
185
- except Exception as e:
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="session_factory",
198
- source_function="DatabaseService.get_session"
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
- swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
231
-
232
- # 모든 필드와 extra_data 분리
233
- for key, value in input_data.items():
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 (Type[Base]): 생성할 모델 클래스
279
- entity_data (Dict[str, Any]): 엔티티 데이터
38
+ model: 모델 클래스
39
+ entity_data: 생성할 엔티티 데이터
280
40
 
281
41
  Returns:
282
- Any: 생성된 엔티티
42
+ 생성된 엔티티
283
43
 
284
44
  Raises:
285
- CustomException: 데이터베이스 오류 발생
45
+ CustomException: 엔티티 생성 실패 시
286
46
  """
287
47
  try:
288
- # 데이터 전처리 및 모델 인스턴스 생성
289
- processed_data = self.preprocess_data(model, entity_data)
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.db.rollback()
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.UNEXPECTED_ERROR,
355
- detail=f"Unexpected error while creating {model.__name__}: {str(e)}",
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.DB_QUERY_ERROR,
64
+ ErrorCode.DB_CREATE_ERROR,
399
65
  detail=str(e),
400
- source_function="DatabaseService.retrieve_entity",
66
+ source_function="DatabaseService.create_entity",
401
67
  original_error=e
402
68
  )
403
69
 
404
- async def update_entity(
70
+ async def get_entity(
405
71
  self,
406
72
  model: Type[Base],
407
- conditions: Dict[str, Any],
408
- update_data: Dict[str, Any]
409
- ) -> Optional[Base]:
410
- """엔티티를 업데이트합니다.
411
-
73
+ filters: Dict[str, Any]
74
+ ) -> Optional[Any]:
75
+ """필터 조건으로 엔티티를 조회합니다.
76
+
412
77
  Args:
413
- model (Type[Base]): 엔티티 모델 클래스
414
- conditions (Dict[str, Any]): 업데이트할 엔티티 조회 조건
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
- Optional[T]: 삭제된 엔티티, 없으면 None
515
-
82
+ 조회된 엔티티 또는 None
83
+
516
84
  Raises:
517
- CustomException: 데이터베이스 오류 발생
85
+ CustomException: 조회 실패
518
86
  """
519
87
  try:
520
- # 1. 엔티티 조회
521
- stmt = select(model).where(
522
- and_(
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.UNEXPECTED_ERROR,
556
- detail=f"Unexpected error while soft deleting {model.__name__}: {str(e)}",
557
- source_function="DatabaseService.soft_delete_entity",
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[T],
564
- skip: int = 0,
565
- limit: int = 100,
101
+ model: Type[Base],
566
102
  filters: Optional[Dict[str, Any]] = None,
567
- joins: Optional[List[Any]] = None
568
- ) -> List[T]:
103
+ skip: int = 0,
104
+ limit: int = 100
105
+ ) -> List[Any]:
569
106
  """엔티티 목록을 조회합니다.
570
107
 
571
108
  Args:
572
- model (Type[T]): 엔티티 모델
573
- skip (int): 건너뛸 레코드 수
574
- limit (int): 조회할 최대 레코드 수
575
- filters (Optional[Dict[str, Any]]): 필터 조건
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
- List[T]: 조회된 엔티티 목록
115
+ 엔티티 목록
583
116
 
584
117
  Raises:
585
- CustomException: 데이터베이스 오류 발생
118
+ CustomException: 조회 실패
586
119
  """
587
120
  try:
588
- query = select(model)
589
- conditions = []
590
-
121
+ stmt = select(model)
591
122
  if filters:
592
- for key, value in filters.items():
593
- if key == "search" and isinstance(value, list):
594
- # 전체 검색 조건
595
- search_conditions = []
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.validate_unique_fields",
131
+ source_function="DatabaseService.list_entities",
697
132
  original_error=e
698
133
  )
699
134
 
700
- async def validate_foreign_key_fields(
135
+ async def update_entity(
701
136
  self,
702
- model: Type[T],
703
- fields: Dict[str, Any]
704
- ) -> None:
705
- """외래 키 필드를 검증합니다.
137
+ entity: Base,
138
+ update_data: Dict[str, Any]
139
+ ) -> Any:
140
+ """엔티티를 수정합니다.
706
141
 
707
142
  Args:
708
- model (Type[T]): 검증할 모델 클래스
709
- fields (Dict[str, Any]): 검증할 외래 키 필드와 값
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
- # 1. 엔티티 조회
799
- stmt = select(model).where(
800
- and_(
801
- model.ulid == entity_id,
802
- model.is_deleted == False
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.DB_DELETE_ERROR,
161
+ ErrorCode.DB_INTEGRITY_ERROR,
848
162
  detail=str(e),
849
- source_function=source_function or "DatabaseService.soft_delete",
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.UNEXPECTED_ERROR,
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=f"{self.__class__.__name__}.execute_query",
171
+ source_function="DatabaseService.update_entity",
914
172
  original_error=e
915
173
  )
916
174
 
917
- async def execute(self, stmt):
918
- """SQL 문을 실행합니다.
175
+ async def delete_entity(
176
+ self,
177
+ entity: Base,
178
+ soft_delete: bool = True
179
+ ) -> bool:
180
+ """엔티티를 삭제합니다.
919
181
 
920
182
  Args:
921
- stmt: 실행할 SQL 문
183
+ entity: 삭제할 엔티티
184
+ soft_delete: 소프트 삭제 여부
922
185
 
923
186
  Returns:
924
- Result: 실행 결과
187
+ 삭제 성공 여부
925
188
 
926
189
  Raises:
927
- CustomException: 데이터베이스 오류 발생
190
+ CustomException: 삭제 실패
928
191
  """
929
192
  try:
930
- return await self.db.execute(stmt)
931
- except SQLAlchemyError as e:
932
- raise CustomException(
933
- ErrorCode.DB_QUERY_ERROR,
934
- detail=str(e),
935
- source_function="DatabaseService.execute",
936
- original_error=e
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.UNEXPECTED_ERROR,
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.refresh",
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
+ )