Dev/Python

SQLAlchemy DB 처리 방식

lumination 2025. 4. 17. 09:59

1. SQLAlchemy + Pydantic 방식

  • Pydantic과 함께 쓰면 API/DB 역할 분리가 명확함
  • DB 데이터를 "객체"로 다룰 수 있음 (User, Post 등)
  • relationship, ForeignKey로 테이블 간 관계 표현 가능
# 1. Pydantic 모델 (입출력용)
class UserCreate(BaseModel):
    username: str
    email: str

# 2. SQLAlchemy ORM 모델 (DB 테이블 대응)
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)

# 3. ORM 방식으로 DB에 객체 저장
def create_user(db: Session, user_data: UserCreate):
    db_user = User(**user_data.dict())  # 딕셔너리 언팩 → ORM 객체 생성
    db.add(db_user)                     # 세션에 추가
    db.commit()                         # DB에 반영
    db.refresh(db_user)                # 새로 생성된 id 등 갱신
    return db_user

  • join, filter, subquery, order_by 등 복잡한 조건
from sqlalchemy.orm import aliased
from sqlalchemy import func, desc

def get_top_users(db: Session):
    # 최근 등록된 유저 중 이메일 있는 애들만, 이름 내림차순
    return db.query(User).filter(
        User.email != None,
        User.username.like("gildong%")
    ).order_by(desc(User.id)).limit(10).all()

  • func, group_by, having 등 집계함수 활용
from sqlalchemy import func

def get_user_count_by_email_domain(db: Session):
    return db.query(
        func.substr(User.email, func.instr(User.email, '@') + 1).label("domain"),
        func.count(User.id).label("count")
    ).group_by("domain").having(func.count(User.id) > 5).all()

  • 쿼리 힌트 사용 (with_hint, prefix_with, FOR UPDATE 등)
def get_user_with_hint(db: Session):
    # MySQL 인덱스 힌트 사용 예시 (DB 종류에 따라 다름)
    query = db.query(User).with_hint(User, "USE INDEX (idx_username)", dialect_name="mysql")
    return query.filter(User.username == "gildong").first()

  • GET /users?skip=20&limit=10
    • skip: 건너뛸 row 수 (예: 0, 10, 20...)
    • limit: 한 번에 가져올 row 수
def get_users_paginated(db: Session, skip: int = 0, limit: int = 10):
    return db.query(User).offset(skip).limit(limit).all()

  • 페이징 예시 - 응답 구조
from pydantic import BaseModel

class PaginatedUsers(BaseModel):
    total: int
    skip: int
    limit: int
    data: List[UserRead]

@router.get("/users", response_model=PaginatedUsers)
def list_users(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    total = db.query(User).count()
    users = db.query(User).offset(skip).limit(limit).all()
    return PaginatedUsers(
        total=total,
        skip=skip,
        limit=limit,
        data=users
    )
  • 응답 구조
    • {
        "total": 100,
        "skip": 20,
        "limit": 10,
        "data": [
          { "id": 21, "username": "hong", ... },
          ...
        ]
      }

  • 조인 (클래스 정의)
# user.py
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String)
    posts = relationship("Post", back_populates="owner")

# post.py
class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    owner_id = Column(Integer, ForeignKey("users.id"))
    owner = relationship("User", back_populates="posts")
  • 조인 사용 예시
# join(Post.owner) 은 내부적으로 posts.owner_id = users.id를 기준으로 조인
def get_posts_with_users(db: Session):
    return db.query(Post).join(Post.owner).all()
    
    
# 유저 정보 포함해서 가져오기 (query(Post, User) → 튜플로 결과 나옴)
def get_posts_with_user_info(db: Session):
    results = db.query(Post, User).join(User, Post.owner_id == User.id).all()

    for post, user in results:
        print(post.title, "-", user.username)
        
        
# outerjoin (LEFT JOIN) 예시
def get_all_users_with_or_without_posts(db: Session):
    return db.query(User).outerjoin(User.posts).all()
    
    
# Pydantic으로 응답 만들기
# joinedload() 써주면 N+1 문제 없이 User와 Post 같이 가져옴
class PostRead(BaseModel):
    id: int
    title: str

class UserReadWithPosts(BaseModel):
    id: int
    username: str
    posts: List[PostRead]

    class Config:
        orm_mode = True
        
@router.get("/users", response_model=List[UserReadWithPosts])
def list_users_with_posts(db: Session = Depends(get_db)):
    return db.query(User).options(joinedload(User.posts)).all()

2. SQLAlchemy + Raw SQL 방식 (가장 Low-level)

  • SQL문을 직접 작성함
  • text()로 감싸서 쿼리 실행
  • 가장 직접적이지만, 유지보수가 어렵고 타입 지원도 없음
from sqlalchemy import text

def create_user_raw_sql(db, username: str, email: str):
    query = text("INSERT INTO users (username, email) VALUES (:username, :email)")
    db.execute(query, {"username": username, "email": email})
    db.commit()

3. SQLAlchemy Core 방식

  • ORM 객체 없이 테이블을 직접 정의
  • SQL을 Python 코드로 조립하는 방식
  • 타입 지원이나 코드 자동화는 어느 정도 있음
from sqlalchemy import Table, Column, Integer, String, MetaData, insert

metadata = MetaData()

user_table = Table(
    "users", metadata,
    Column("id", Integer, primary_key=True),
    Column("username", String),
    Column("email", String)
)

def create_user_core(db, username: str, email: str):
    stmt = insert(user_table).values(username=username, email=email)
    db.execute(stmt)
    db.commit()