본문으로 건너뛰기
Advertisement

SQLAlchemy 2.0

SQLAlchemy는 Python의 가장 강력한 ORM 라이브러리입니다. 2.0 버전부터 선언형 매핑async 지원이 크게 개선되었습니다.


설치와 기본 설정

pip install sqlalchemy psycopg2-binary  # PostgreSQL
# 또는
pip install sqlalchemy aiosqlite # SQLite (비동기)
pip install sqlalchemy asyncpg # PostgreSQL (비동기)
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase

# 동기 엔진
engine = create_engine(
"postgresql://user:password@localhost/dbname",
pool_size=10,
max_overflow=20,
echo=False, # SQL 로그 출력 여부
)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)


class Base(DeclarativeBase):
pass

모델 선언형 매핑

from sqlalchemy import String, Integer, ForeignKey, Text, Boolean, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from datetime import datetime


class Category(Base):
__tablename__ = "categories"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(100), unique=True)

products: Mapped[list["Product"]] = relationship(back_populates="category")

def __repr__(self):
return f"<Category(id={self.id}, name={self.name})>"


class Product(Base):
__tablename__ = "products"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
price: Mapped[float] = mapped_column(nullable=False)
stock: Mapped[int] = mapped_column(default=0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)

category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
category: Mapped["Category | None"] = relationship(back_populates="products")

def __repr__(self):
return f"<Product(id={self.id}, name={self.name})>"

테이블 생성 및 세션 관리

# 테이블 생성
Base.metadata.create_all(bind=engine)

# 세션 컨텍스트 매니저 (FastAPI / 일반 앱 공통)
from contextlib import contextmanager


@contextmanager
def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()

CRUD 패턴

from sqlalchemy.orm import Session
from sqlalchemy import select, update, delete


# CREATE
def create_product(db: Session, name: str, price: float, category_id: int) -> Product:
product = Product(name=name, price=price, category_id=category_id)
db.add(product)
db.flush() # DB에 반영 (commit 전)
db.refresh(product) # auto-generated 값(id, created_at) 가져오기
return product


# READ — select() 스타일 (2.0 권장)
def get_product(db: Session, product_id: int) -> Product | None:
stmt = select(Product).where(Product.id == product_id)
return db.scalar(stmt)


def get_products(
db: Session,
skip: int = 0,
limit: int = 20,
is_active: bool = True,
) -> list[Product]:
stmt = (
select(Product)
.where(Product.is_active == is_active)
.order_by(Product.created_at.desc())
.offset(skip)
.limit(limit)
)
return list(db.scalars(stmt))


# 관계 데이터 함께 조회 (N+1 방지)
from sqlalchemy.orm import selectinload, joinedload


def get_products_with_category(db: Session) -> list[Product]:
stmt = (
select(Product)
.options(joinedload(Product.category)) # JOIN으로 한 번에
.where(Product.is_active == True)
)
return list(db.scalars(stmt).unique())


# UPDATE
def update_product(db: Session, product_id: int, **kwargs) -> Product | None:
product = get_product(db, product_id)
if not product:
return None
for key, value in kwargs.items():
setattr(product, key, value)
db.flush()
db.refresh(product)
return product


# 벌크 UPDATE
def deactivate_products(db: Session, category_id: int) -> int:
stmt = (
update(Product)
.where(Product.category_id == category_id)
.values(is_active=False)
.execution_options(synchronize_session="fetch")
)
result = db.execute(stmt)
return result.rowcount


# DELETE
def delete_product(db: Session, product_id: int) -> bool:
product = get_product(db, product_id)
if not product:
return False
db.delete(product)
return True

집계와 서브쿼리

from sqlalchemy import func, and_, or_


# 집계
def get_category_stats(db: Session) -> list[dict]:
stmt = (
select(
Category.name,
func.count(Product.id).label("product_count"),
func.avg(Product.price).label("avg_price"),
func.sum(Product.stock).label("total_stock"),
)
.join(Product, isouter=True)
.group_by(Category.id)
.having(func.count(Product.id) > 0)
.order_by(func.count(Product.id).desc())
)
rows = db.execute(stmt).all()
return [row._asdict() for row in rows]


# 서브쿼리
def get_expensive_products(db: Session) -> list[Product]:
avg_price_subq = select(func.avg(Product.price)).scalar_subquery()
stmt = select(Product).where(Product.price > avg_price_subq)
return list(db.scalars(stmt))

비동기 SQLAlchemy

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker

# 비동기 엔진
async_engine = create_async_engine(
"postgresql+asyncpg://user:password@localhost/dbname",
echo=False,
)

AsyncSessionLocal = async_sessionmaker(
bind=async_engine, expire_on_commit=False
)


# FastAPI 의존성
async def get_async_db():
async with AsyncSessionLocal() as db:
try:
yield db
await db.commit()
except Exception:
await db.rollback()
raise


# 비동기 쿼리
async def get_product_async(db: AsyncSession, product_id: int) -> Product | None:
stmt = (
select(Product)
.options(selectinload(Product.category)) # 비동기에선 selectinload 권장
.where(Product.id == product_id)
)
return await db.scalar(stmt)

정리

기능방법
모델 정의Mapped[type] = mapped_column(...)
단일 조회db.scalar(select(Model).where(...))
복수 조회db.scalars(stmt)
관계 즉시 로딩joinedload() / selectinload()
벌크 업데이트update(Model).where(...).values(...)
집계func.count(), func.avg(), group_by()
비동기AsyncSession + create_async_engine

SQLAlchemy 2.0의 select() 스타일이 타입 안전성과 IDE 자동 완성이 우수하여 표준으로 권장됩니다.

Advertisement