aiteamutils 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,823 @@
1
+ from typing import Any, Dict, Optional, Type, AsyncGenerator, TypeVar, List, Union
2
+ from sqlalchemy import select, update, and_, Table
3
+ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, AsyncEngine
4
+ from sqlalchemy.orm import sessionmaker, Load, joinedload
5
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
6
+ from sqlalchemy.pool import QueuePool
7
+ from contextlib import asynccontextmanager
8
+ from sqlalchemy import or_
9
+ from fastapi import Request
10
+ from ulid import ULID
11
+ from sqlalchemy.sql import Select
12
+
13
+ from .exceptions import ErrorCode, CustomException
14
+ from .base_model import Base, BaseColumn
15
+ from .enums import ActivityType
16
+ from .config import settings
17
+
18
+ T = TypeVar("T", bound=BaseColumn)
19
+
20
+ class DatabaseService:
21
+ def __init__(self, db_url: str):
22
+ """DatabaseService 초기화.
23
+
24
+ Args:
25
+ db_url: 데이터베이스 URL
26
+ """
27
+ self.engine = create_async_engine(
28
+ db_url,
29
+ echo=settings.DB_ECHO,
30
+ pool_size=settings.DB_POOL_SIZE,
31
+ max_overflow=settings.DB_MAX_OVERFLOW,
32
+ pool_timeout=settings.DB_POOL_TIMEOUT,
33
+ pool_recycle=settings.DB_POOL_RECYCLE,
34
+ pool_pre_ping=True,
35
+ poolclass=QueuePool,
36
+ )
37
+
38
+ self.async_session = sessionmaker(
39
+ bind=self.engine,
40
+ class_=AsyncSession,
41
+ expire_on_commit=False
42
+ )
43
+
44
+ @asynccontextmanager
45
+ async def get_db(self) -> AsyncGenerator[AsyncSession, None]:
46
+ """데이터베이스 세션을 생성하고 반환하는 비동기 제너레이터."""
47
+ async with self.async_session() as session:
48
+ try:
49
+ yield session
50
+ finally:
51
+ await session.close()
52
+
53
+ def preprocess_data(self, model: Type[Base], input_data: Dict[str, Any], existing_data: Dict[str, Any] = None) -> Dict[str, Any]:
54
+ """입력 데이터를 전처리하여 extra_data로 분리
55
+
56
+ Args:
57
+ model (Type[Base]): SQLAlchemy 모델 클래스
58
+ input_data (Dict[str, Any]): 입력 데이터
59
+ existing_data (Dict[str, Any], optional): 기존 데이터. Defaults to None.
60
+
61
+ Returns:
62
+ Dict[str, Any]: 전처리된 데이터
63
+ """
64
+ model_attrs = {
65
+ attr for attr in dir(model)
66
+ if not attr.startswith('_') and not callable(getattr(model, attr))
67
+ }
68
+ model_data = {}
69
+ extra_data = {}
70
+
71
+ # 기존 extra_data가 있으면 복사
72
+ if existing_data and "extra_data" in existing_data:
73
+ extra_data = existing_data["extra_data"].copy()
74
+
75
+ # 스웨거 자동생성 필드 패턴
76
+ swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
77
+
78
+ # 모든 필드와 extra_data 분리
79
+ for key, value in input_data.items():
80
+ # 스웨거 자동생성 필드는 무시
81
+ if key in swagger_patterns:
82
+ continue
83
+
84
+ if key in model_attrs:
85
+ model_data[key] = value
86
+ else:
87
+ extra_data[key] = value
88
+
89
+ # extra_data가 있고, 모델이 extra_data 속성을 가지고 있으면 추가
90
+ if extra_data and "extra_data" in model_attrs:
91
+ model_data["extra_data"] = extra_data
92
+
93
+ return model_data
94
+
95
+ ############################
96
+ # 2. 트랜잭션 및 세션 관리 #
97
+ ############################
98
+ @asynccontextmanager
99
+ async def transaction(self):
100
+ """트랜잭션 컨텍스트 매니저
101
+
102
+ 트랜잭션 범위를 명시적으로 관리합니다.
103
+ with 문을 벗어날 때 자동으로 commit 또는 rollback됩니다.
104
+
105
+ Example:
106
+ async with db_service.transaction():
107
+ await db_service.create_entity(...)
108
+ await db_service.update_entity(...)
109
+ """
110
+ try:
111
+ yield
112
+ await self.db.commit()
113
+ except Exception as e:
114
+ await self.db.rollback()
115
+ raise e
116
+
117
+ #######################
118
+ # 3. 데이터 처리 #
119
+ #######################
120
+ async def create_entity(self, model: Type[Base], entity_data: Dict[str, Any]) -> Any:
121
+ """새로운 엔티티를 생성합니다.
122
+
123
+ Args:
124
+ model (Type[Base]): 생성할 모델 클래스
125
+ entity_data (Dict[str, Any]): 엔티티 데이터
126
+
127
+ Returns:
128
+ Any: 생성된 엔티티
129
+
130
+ Raises:
131
+ CustomException: 데이터베이스 오류 발생
132
+ """
133
+ try:
134
+ # 데이터 전처리 및 모델 인스턴스 생성
135
+ processed_data = self.preprocess_data(model, entity_data)
136
+
137
+ # 외래 키 필드 검증
138
+ foreign_key_fields = {
139
+ field: value for field, value in processed_data.items()
140
+ if any(fk.parent.name == field for fk in model.__table__.foreign_keys)
141
+ }
142
+ if foreign_key_fields:
143
+ await self.validate_foreign_key_fields(model, foreign_key_fields)
144
+
145
+ entity = model(**processed_data)
146
+
147
+ self.db.add(entity)
148
+ await self.db.flush()
149
+ await self.db.commit()
150
+ await self.db.refresh(entity)
151
+ return entity
152
+ except IntegrityError as e:
153
+ await self.db.rollback()
154
+ error_str = str(e)
155
+
156
+ if "duplicate key" in error_str.lower():
157
+ # 중복 키 에러 처리
158
+ field = None
159
+ value = None
160
+
161
+ # 에러 메시지에서 필드와 값 추출
162
+ if "Key (" in error_str and ") already exists" in error_str:
163
+ field_value = error_str.split("Key (")[1].split(") already exists")[0]
164
+ if "=" in field_value:
165
+ field, value = field_value.split("=")
166
+ field = field.strip("() ")
167
+ value = value.strip("() ")
168
+
169
+ if not field:
170
+ field = "id" # 기본값
171
+ value = str(entity_data.get(field, ""))
172
+
173
+ raise CustomException(
174
+ ErrorCode.DUPLICATE_ERROR,
175
+ detail=f"{model.__tablename__}|{field}|{value}",
176
+ source_function="DatabaseService.create_entity",
177
+ original_error=e
178
+ )
179
+ elif "violates foreign key constraint" in error_str.lower():
180
+ # 외래키 위반 에러는 validate_foreign_key_fields에서 이미 처리됨
181
+ raise CustomException(
182
+ ErrorCode.FOREIGN_KEY_VIOLATION,
183
+ detail=error_str,
184
+ source_function="DatabaseService.create_entity",
185
+ original_error=e
186
+ )
187
+ else:
188
+ raise CustomException(
189
+ ErrorCode.DB_CREATE_ERROR,
190
+ detail=f"Failed to create {model.__name__}: {str(e)}",
191
+ source_function="DatabaseService.create_entity",
192
+ original_error=e
193
+ )
194
+ except CustomException as e:
195
+ await self.db.rollback()
196
+ raise e
197
+ except Exception as e:
198
+ await self.db.rollback()
199
+ raise CustomException(
200
+ ErrorCode.UNEXPECTED_ERROR,
201
+ detail=f"Unexpected error while creating {model.__name__}: {str(e)}",
202
+ source_function="DatabaseService.create_entity",
203
+ original_error=e
204
+ )
205
+
206
+ async def retrieve_entity(
207
+ self,
208
+ model: Type[T],
209
+ conditions: Dict[str, Any],
210
+ join_options: Optional[Union[Load, List[Load]]] = None
211
+ ) -> Optional[T]:
212
+ """조건에 맞는 단일 엔티티를 조회합니다.
213
+
214
+ Args:
215
+ model: 모델 클래스
216
+ conditions: 조회 조건
217
+ join_options: SQLAlchemy의 joinedload 옵션 또는 옵션 리스트
218
+
219
+ Returns:
220
+ Optional[T]: 조회된 엔티티 또는 None
221
+
222
+ Raises:
223
+ CustomException: 데이터베이스 작업 중 오류 발생 시
224
+ """
225
+ try:
226
+ stmt = select(model)
227
+
228
+ # Join 옵션 적용
229
+ if join_options is not None:
230
+ if isinstance(join_options, list):
231
+ stmt = stmt.options(*join_options)
232
+ else:
233
+ stmt = stmt.options(join_options)
234
+
235
+ # 조건 적용
236
+ for key, value in conditions.items():
237
+ stmt = stmt.where(getattr(model, key) == value)
238
+
239
+ result = await self.execute_query(stmt)
240
+ return result.unique().scalar_one_or_none()
241
+
242
+ except Exception as e:
243
+ raise CustomException(
244
+ ErrorCode.DB_QUERY_ERROR,
245
+ detail=str(e),
246
+ source_function="DatabaseService.retrieve_entity",
247
+ original_error=e
248
+ )
249
+
250
+ async def update_entity(
251
+ self,
252
+ model: Type[Base],
253
+ conditions: Dict[str, Any],
254
+ update_data: Dict[str, Any]
255
+ ) -> Optional[Base]:
256
+ """엔티티를 업데이트합니다.
257
+
258
+ Args:
259
+ model (Type[Base]): 엔티티 모델 클래스
260
+ conditions (Dict[str, Any]): 업데이트할 엔티티 조회 조건
261
+ update_data (Dict[str, Any]): 업데이트할 데이터
262
+
263
+ Returns:
264
+ Optional[Base]: 업데이트된 엔티티
265
+
266
+ Raises:
267
+ CustomException: 데이터베이스 오류 발생
268
+ """
269
+ try:
270
+ # 엔티티 조회
271
+ stmt = select(model)
272
+ for key, value in conditions.items():
273
+ stmt = stmt.where(getattr(model, key) == value)
274
+
275
+ result = await self.db.execute(stmt)
276
+ entity = result.scalar_one_or_none()
277
+
278
+ if not entity:
279
+ return None
280
+
281
+ # 기존 데이터를 딕셔너리로 변환
282
+ existing_data = {
283
+ column.name: getattr(entity, column.name)
284
+ for column in entity.__table__.columns
285
+ }
286
+
287
+ # 데이터 전처리
288
+ processed_data = self.preprocess_data(model, update_data, existing_data)
289
+
290
+ # UPDATE 문 생성 및 실행
291
+ update_stmt = (
292
+ update(model)
293
+ .where(and_(*[getattr(model, key) == value for key, value in conditions.items()]))
294
+ .values(**processed_data)
295
+ .returning(model)
296
+ )
297
+
298
+ result = await self.db.execute(update_stmt)
299
+ await self.db.commit()
300
+
301
+ # 업데이트된 엔티티 반환
302
+ updated_entity = result.scalar_one()
303
+ return updated_entity
304
+
305
+ except SQLAlchemyError as e:
306
+ await self.db.rollback()
307
+ raise CustomException(
308
+ ErrorCode.DB_UPDATE_ERROR,
309
+ detail=f"Failed to update {model.__name__}: {str(e)}",
310
+ source_function="DatabaseService.update_entity",
311
+ original_error=e
312
+ )
313
+ except Exception as e:
314
+ await self.db.rollback()
315
+ raise CustomException(
316
+ ErrorCode.UNEXPECTED_ERROR,
317
+ detail=f"Unexpected error while updating {model.__name__}: {str(e)}",
318
+ source_function="DatabaseService.update_entity",
319
+ original_error=e
320
+ )
321
+
322
+ async def delete_entity(self, entity: T) -> None:
323
+ """엔티티를 실제로 삭제합니다.
324
+
325
+ Args:
326
+ entity (T): 삭제할 엔티티
327
+
328
+ Raises:
329
+ CustomException: 데이터베이스 오류 발생
330
+ """
331
+ try:
332
+ await self.db.delete(entity)
333
+ await self.db.flush()
334
+ await self.db.commit()
335
+ except SQLAlchemyError as e:
336
+ await self.db.rollback()
337
+ raise CustomException(
338
+ ErrorCode.DB_DELETE_ERROR,
339
+ detail=f"Failed to delete entity: {str(e)}",
340
+ source_function="DatabaseService.delete_entity",
341
+ original_error=e
342
+ )
343
+ except Exception as e:
344
+ await self.db.rollback()
345
+ raise CustomException(
346
+ ErrorCode.UNEXPECTED_ERROR,
347
+ detail=f"Unexpected error while deleting entity: {str(e)}",
348
+ source_function="DatabaseService.delete_entity",
349
+ original_error=e
350
+ )
351
+
352
+ async def soft_delete_entity(self, model: Type[T], ulid: str) -> Optional[T]:
353
+ """엔티티를 소프트 삭제합니다 (is_deleted = True).
354
+
355
+ Args:
356
+ model (Type[T]): 엔티티 모델
357
+ ulid (str): 삭제할 엔티티의 ULID
358
+
359
+ Returns:
360
+ Optional[T]: 삭제된 엔티티, 없으면 None
361
+
362
+ Raises:
363
+ CustomException: 데이터베이스 오류 발생
364
+ """
365
+ try:
366
+ # 1. 엔티티 조회
367
+ stmt = select(model).where(
368
+ and_(
369
+ model.ulid == ulid,
370
+ model.is_deleted == False
371
+ )
372
+ )
373
+ result = await self.db.execute(stmt)
374
+ entity = result.scalar_one_or_none()
375
+
376
+ if not entity:
377
+ return None
378
+
379
+ # 2. 소프트 삭제 처리
380
+ stmt = update(model).where(
381
+ model.ulid == ulid
382
+ ).values(
383
+ is_deleted=True
384
+ )
385
+ await self.db.execute(stmt)
386
+ await self.db.commit()
387
+
388
+ # 3. 업데이트된 엔티티 반환
389
+ return entity
390
+ except SQLAlchemyError as e:
391
+ await self.db.rollback()
392
+ raise CustomException(
393
+ ErrorCode.DB_DELETE_ERROR,
394
+ detail=f"Failed to soft delete {model.__name__}: {str(e)}",
395
+ source_function="DatabaseService.soft_delete_entity",
396
+ original_error=e
397
+ )
398
+ except Exception as e:
399
+ await self.db.rollback()
400
+ raise CustomException(
401
+ ErrorCode.UNEXPECTED_ERROR,
402
+ detail=f"Unexpected error while soft deleting {model.__name__}: {str(e)}",
403
+ source_function="DatabaseService.soft_delete_entity",
404
+ original_error=e
405
+ )
406
+
407
+ async def list_entities(
408
+ self,
409
+ model: Type[T],
410
+ skip: int = 0,
411
+ limit: int = 100,
412
+ filters: Optional[Dict[str, Any]] = None,
413
+ joins: Optional[List[Any]] = None
414
+ ) -> List[T]:
415
+ """엔티티 목록을 조회합니다.
416
+
417
+ Args:
418
+ model (Type[T]): 엔티티 모델
419
+ skip (int): 건너뛸 레코드 수
420
+ limit (int): 조회할 최대 레코드 수
421
+ filters (Optional[Dict[str, Any]]): 필터 조건
422
+ - field: value -> field = value
423
+ - field__ilike: value -> field ILIKE value
424
+ - search: [(field, pattern), ...] -> OR(field ILIKE pattern, ...)
425
+ joins (Optional[List[Any]]): 조인할 관계들 (joinedload 객체 리스트)
426
+
427
+ Returns:
428
+ List[T]: 조회된 엔티티 목록
429
+
430
+ Raises:
431
+ CustomException: 데이터베이스 오류 발생
432
+ """
433
+ try:
434
+ query = select(model)
435
+ conditions = []
436
+
437
+ if filters:
438
+ for key, value in filters.items():
439
+ if key == "search" and isinstance(value, list):
440
+ # 전체 검색 조건
441
+ search_conditions = []
442
+ for field_name, pattern in value:
443
+ field = getattr(model, field_name)
444
+ search_conditions.append(field.ilike(pattern))
445
+ if search_conditions:
446
+ conditions.append(or_(*search_conditions))
447
+ elif "__ilike" in key:
448
+ # ILIKE 검색
449
+ field_name = key.replace("__ilike", "")
450
+ field = getattr(model, field_name)
451
+ conditions.append(field.ilike(value))
452
+ else:
453
+ # 일반 필터
454
+ field = getattr(model, key)
455
+ conditions.append(field == value)
456
+
457
+ if conditions:
458
+ query = query.where(and_(*conditions))
459
+
460
+ if joins:
461
+ for join_option in joins:
462
+ query = query.options(join_option)
463
+
464
+ query = query.offset(skip).limit(limit)
465
+ result = await self.db.execute(query)
466
+ return result.scalars().unique().all()
467
+ except SQLAlchemyError as e:
468
+ raise CustomException(
469
+ ErrorCode.DB_READ_ERROR,
470
+ detail=f"Failed to list {model.__name__}: {str(e)}",
471
+ source_function="DatabaseService.list_entities",
472
+ original_error=e
473
+ )
474
+ except Exception as e:
475
+ raise CustomException(
476
+ ErrorCode.UNEXPECTED_ERROR,
477
+ detail=f"Unexpected error while listing {model.__name__}: {str(e)}",
478
+ source_function="DatabaseService.list_entities",
479
+ original_error=e
480
+ )
481
+
482
+ ######################
483
+ # 4. 검증 #
484
+ ######################
485
+ async def validate_unique_fields(
486
+ self,
487
+ table_or_model: Union[Table, Type[Any]],
488
+ fields: Dict[str, Any],
489
+ source_function: str,
490
+ error_code: ErrorCode = ErrorCode.DUPLICATE_ERROR
491
+ ) -> None:
492
+ """
493
+ 데이터베이스에서 필드의 유일성을 검증합니다.
494
+
495
+ Args:
496
+ table_or_model: 검증할 테이블 또는 모델 클래스
497
+ fields: 검증할 필드와 값의 딕셔너리 {"field_name": value}
498
+ source_function: 호출한 함수명
499
+ error_code: 사용할 에러 코드 (기본값: DUPLICATE_ERROR)
500
+
501
+ Raises:
502
+ CustomException: 중복된 값이 존재할 경우
503
+ """
504
+ try:
505
+ conditions = []
506
+ for field_name, value in fields.items():
507
+ conditions.append(getattr(table_or_model, field_name) == value)
508
+
509
+ query = select(table_or_model).where(or_(*conditions))
510
+ result = await self.db.execute(query)
511
+ existing = result.scalar_one_or_none()
512
+
513
+ if existing:
514
+ table_name = table_or_model.name if hasattr(table_or_model, 'name') else table_or_model.__tablename__
515
+ # 단일 필드인 경우
516
+ if len(fields) == 1:
517
+ field_name, value = next(iter(fields.items()))
518
+ detail = f"{table_name}|{field_name}|{value}"
519
+ # 복수 필드인 경우
520
+ else:
521
+ fields_str = "|".join(f"{k}:{v}" for k, v in fields.items())
522
+ detail = f"{table_name}|{fields_str}"
523
+
524
+ raise CustomException(
525
+ error_code,
526
+ detail=detail,
527
+ source_function="DatabaseService.validate_unique_fields"
528
+ )
529
+
530
+ except CustomException as e:
531
+ raise CustomException(
532
+ e.error_code,
533
+ detail=e.detail,
534
+ source_function="DatabaseService.validate_unique_fields",
535
+ original_error=e.original_error,
536
+ parent_source_function=e.source_function
537
+ )
538
+ except Exception as e:
539
+ raise CustomException(
540
+ ErrorCode.DB_QUERY_ERROR,
541
+ detail=str(e),
542
+ source_function="DatabaseService.validate_unique_fields",
543
+ original_error=e
544
+ )
545
+
546
+ async def validate_foreign_key_fields(
547
+ self,
548
+ model: Type[T],
549
+ fields: Dict[str, Any]
550
+ ) -> None:
551
+ """외래 키 필드를 검증합니다.
552
+
553
+ Args:
554
+ model (Type[T]): 검증할 모델 클래스
555
+ fields (Dict[str, Any]): 검증할 외래 키 필드와 값
556
+
557
+ Raises:
558
+ CustomException: 참조하는 레코드가 존재하지 않는 경우
559
+ """
560
+ for field, value in fields.items():
561
+ # 외래 키 관계 정보 가져오기
562
+ foreign_key = next(
563
+ (fk for fk in model.__table__.foreign_keys if fk.parent.name == field),
564
+ None
565
+ )
566
+ if foreign_key and value:
567
+ # 참조하는 테이블에서 레코드 존재 여부 확인
568
+ referenced_table = foreign_key.column.table
569
+ query = select(referenced_table).where(
570
+ and_(
571
+ foreign_key.column == value,
572
+ getattr(referenced_table.c, 'is_deleted', None) == False
573
+ )
574
+ )
575
+ result = await self.db.execute(query)
576
+ if not result.scalar_one_or_none():
577
+ raise CustomException(
578
+ ErrorCode.FOREIGN_KEY_VIOLATION,
579
+ detail=f"{referenced_table.name}|{field}|{value}",
580
+ source_function="DatabaseService.validate_foreign_key_fields"
581
+ )
582
+
583
+ #######################
584
+ # 5. 쿼리 실행 #
585
+ #######################
586
+ async def create_log(self, model: Type[Base], log_data: Dict[str, Any], request: Request = None) -> None:
587
+ """로그를 생성합니다.
588
+
589
+ Args:
590
+ model: 로그 모델 클래스
591
+ log_data: 로그 데이터
592
+ request: FastAPI 요청 객체
593
+
594
+ Returns:
595
+ 생성된 로그 엔티티
596
+ """
597
+ # 공통 필드 추가 (ULID를 문자열로 변환)
598
+ log_data["ulid"] = str(ULID())
599
+
600
+ # request가 있는 경우 user-agent와 ip 정보 추가
601
+ if request:
602
+ log_data["user_agent"] = request.headers.get("user-agent")
603
+ log_data["ip_address"] = request.headers.get("x-forwarded-for")
604
+
605
+ return await self.create_entity(model, log_data)
606
+
607
+ async def soft_delete(
608
+ self,
609
+ model: Type[T],
610
+ entity_id: str,
611
+ source_function: str = None,
612
+ request: Request = None
613
+ ) -> None:
614
+ """엔티티를 소프트 삭제합니다.
615
+
616
+ Args:
617
+ model: 모델 클래스
618
+ entity_id: 삭제할 엔티티의 ID
619
+ source_function: 호출한 함수명
620
+ request: FastAPI 요청 객체
621
+
622
+ Raises:
623
+ CustomException: 데이터베이스 작업 실패 시
624
+ """
625
+ try:
626
+ # 1. 엔티티 조회
627
+ stmt = select(model).where(
628
+ and_(
629
+ model.ulid == entity_id,
630
+ model.is_deleted == False
631
+ )
632
+ )
633
+ result = await self.db.execute(stmt)
634
+ entity = result.scalar_one_or_none()
635
+
636
+ if not entity:
637
+ raise CustomException(
638
+ ErrorCode.NOT_FOUND,
639
+ detail=f"{model.__name__}|{entity_id}",
640
+ source_function=source_function or "DatabaseService.soft_delete"
641
+ )
642
+
643
+ # 2. 소프트 삭제 처리
644
+ stmt = update(model).where(
645
+ model.ulid == entity_id
646
+ ).values(
647
+ is_deleted=True
648
+ )
649
+ await self.db.execute(stmt)
650
+ await self.db.commit()
651
+
652
+ # 3. 삭제 로그 생성
653
+ if request:
654
+ activity_type = f"{model.__tablename__.upper()}_DELETED"
655
+ await self.create_log({
656
+ "type": activity_type,
657
+ "fk_table": model.__tablename__,
658
+ "extra_data": {
659
+ f"{model.__tablename__}_ulid": entity_id
660
+ }
661
+ }, request)
662
+
663
+ except CustomException as e:
664
+ await self.db.rollback()
665
+ raise CustomException(
666
+ e.error_code,
667
+ detail=e.detail,
668
+ source_function=source_function or "DatabaseService.soft_delete",
669
+ original_error=e.original_error,
670
+ parent_source_function=e.source_function
671
+ )
672
+ except Exception as e:
673
+ await self.db.rollback()
674
+ raise CustomException(
675
+ ErrorCode.DB_DELETE_ERROR,
676
+ detail=str(e),
677
+ source_function=source_function or "DatabaseService.soft_delete",
678
+ original_error=e
679
+ )
680
+
681
+ async def get_entity(self, model: Type[T], ulid: str) -> Optional[T]:
682
+ """ULID로 엔티티를 조회합니다.
683
+
684
+ Args:
685
+ model (Type[T]): 엔티티 모델
686
+ ulid (str): 조회할 엔티티의 ULID
687
+
688
+ Returns:
689
+ Optional[T]: 조회된 엔티티 또는 None
690
+
691
+ Raises:
692
+ CustomException: 데이터베이스 오류 발생
693
+ """
694
+ try:
695
+ query = select(model).where(
696
+ and_(
697
+ model.ulid == ulid,
698
+ model.is_deleted == False
699
+ )
700
+ )
701
+ result = await self.db.execute(query)
702
+ return result.scalar_one_or_none()
703
+ except SQLAlchemyError as e:
704
+ raise CustomException(
705
+ ErrorCode.DB_READ_ERROR,
706
+ detail=f"Failed to get {model.__name__}: {str(e)}",
707
+ source_function="DatabaseService.get_entity",
708
+ original_error=e
709
+ )
710
+ except Exception as e:
711
+ raise CustomException(
712
+ ErrorCode.UNEXPECTED_ERROR,
713
+ detail=f"Unexpected error while getting {model.__name__}: {str(e)}",
714
+ source_function="DatabaseService.get_entity",
715
+ original_error=e
716
+ )
717
+
718
+ async def execute_query(self, query: Select) -> Any:
719
+ """SQL 쿼리를 실행하고 결과를 반환합니다.
720
+
721
+ Args:
722
+ query (Select): 실행할 SQLAlchemy 쿼리
723
+
724
+ Returns:
725
+ Any: 쿼리 실행 결과
726
+
727
+ Raises:
728
+ CustomException: 데이터베이스 작업 중 오류 발생 시
729
+ """
730
+ try:
731
+ async with self.db as session:
732
+ result = await session.execute(query)
733
+ return result
734
+ except Exception as e:
735
+ raise CustomException(
736
+ ErrorCode.DB_QUERY_ERROR,
737
+ detail=str(e),
738
+ source_function=f"{self.__class__.__name__}.execute_query",
739
+ original_error=e
740
+ )
741
+
742
+ async def execute(self, stmt):
743
+ """SQL 문을 실행합니다.
744
+
745
+ Args:
746
+ stmt: 실행할 SQL 문
747
+
748
+ Returns:
749
+ Result: 실행 결과
750
+
751
+ Raises:
752
+ CustomException: 데이터베이스 오류 발생
753
+ """
754
+ try:
755
+ return await self.db.execute(stmt)
756
+ except SQLAlchemyError as e:
757
+ raise CustomException(
758
+ ErrorCode.DB_QUERY_ERROR,
759
+ detail=str(e),
760
+ source_function="DatabaseService.execute",
761
+ original_error=e
762
+ )
763
+ except Exception as e:
764
+ raise CustomException(
765
+ ErrorCode.UNEXPECTED_ERROR,
766
+ detail=str(e),
767
+ source_function="DatabaseService.execute",
768
+ original_error=e
769
+ )
770
+
771
+ async def commit(self) -> None:
772
+ """현재 세션의 변경사항을 데이터베이스에 커밋합니다."""
773
+ try:
774
+ await self.db.commit()
775
+ except SQLAlchemyError as e:
776
+ await self.db.rollback()
777
+ raise CustomException(
778
+ ErrorCode.DB_QUERY_ERROR,
779
+ detail=str(e),
780
+ source_function="DatabaseService.commit",
781
+ original_error=e
782
+ )
783
+
784
+ async def rollback(self) -> None:
785
+ """현재 세션의 변경사항을 롤백합니다."""
786
+ try:
787
+ await self.db.rollback()
788
+ except SQLAlchemyError as e:
789
+ raise CustomException(
790
+ ErrorCode.DB_QUERY_ERROR,
791
+ detail=str(e),
792
+ source_function="DatabaseService.rollback",
793
+ original_error=e
794
+ )
795
+
796
+ async def flush(self) -> None:
797
+ """현재 세션의 변경사항을 데이터베이스에 플러시합니다."""
798
+ try:
799
+ await self.db.flush()
800
+ except SQLAlchemyError as e:
801
+ await self.db.rollback()
802
+ raise CustomException(
803
+ ErrorCode.DB_QUERY_ERROR,
804
+ detail=str(e),
805
+ source_function="DatabaseService.flush",
806
+ original_error=e
807
+ )
808
+
809
+ async def refresh(self, entity: Any) -> None:
810
+ """엔티티를 데이터베이스의 최신 상태로 리프레시합니다.
811
+
812
+ Args:
813
+ entity: 리프레시할 엔티티
814
+ """
815
+ try:
816
+ await self.db.refresh(entity)
817
+ except SQLAlchemyError as e:
818
+ raise CustomException(
819
+ ErrorCode.DB_QUERY_ERROR,
820
+ detail=str(e),
821
+ source_function="DatabaseService.refresh",
822
+ original_error=e
823
+ )