aiteamutils 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ )