Alert
์ด ๊ธ์ Claude Code์ ๋์์ ๋ฐ์ ์์ฑ๋์์ต๋๋ค
TL;DR
- SQLAlchemy๋ Python์ ์ฌ์ค์ ํ์ค ORM
- Core(SQL ํํ์)์ ORM(๊ฐ์ฒด ๋งคํ) ๋ ๋ ์ด์ด๋ก ๊ตฌ์ฑ
- 2.0 ์คํ์ผ์์ select() ๊ธฐ๋ฐ ์ฟผ๋ฆฌ๊ฐ ๊ธฐ๋ณธ
- Alembic์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์ ๊ด๋ฆฌ
SQLAlchemy?
- Python์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํดํท ๊ฒธ ORM
- 2006๋ ์ฒซ ๋ฆด๋ฆฌ์ค, ํ์ฌ 2.x ๋ฒ์
- PostgreSQL, MySQL, SQLite ๋ฑ ์ฃผ์ RDBMS ์ง์
- https://www.sqlalchemy.org/
1. ORM์ด๋
ORM(Object-Relational Mapping)์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์ Python ํด๋์ค๋ก ๋งคํํ๋ ๊ธฐ๋ฒ์ด๋ค. SQL์ ์ง์ ์ฐ์ง ์๊ณ Python ์ฝ๋๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ค๋ฃฐ ์ ์๋ค.
# SQL๋ก ์ง์ ์์ฑ
cursor.execute("SELECT * FROM users WHERE age > 20")
# ORM์ผ๋ก ์์ฑ
session.query(User).filter(User.age > 20).all()ORM์ ์ฅ๋จ์
- ์ฅ์ : ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ข ๋ฅ์ ๋ ๋ฆฝ์ , ์ฝ๋ ๊ฐ๋ ์ฑ ํฅ์, SQL ์ธ์ ์ ๋ฐฉ์ง
- ๋จ์ : ๋ณต์กํ ์ฟผ๋ฆฌ์์ ์ฑ๋ฅ ์ค๋ฒํค๋ ๊ฐ๋ฅ, SQL ์์ฒด๋ฅผ ๋ชจ๋ฅด๋ฉด ๋๋ฒ๊น ์ด๋ ค์
2. Python ORM ์ ํ์ง
| ORM | ํน์ง |
|---|---|
| SQLAlchemy | ๋ ๋ฆฝํ ํ์ค. Flask, FastAPI ๋ฑ ํ๋ ์์ํฌ ๊ฐ๋ฆฌ์ง ์๊ณ ์ฌ์ฉ |
| Django ORM | Django ํ๋ ์์ํฌ ๋ด์ฅ. Django ๋ฐ์์๋ ์ฌ์ฉํ๊ธฐ ๋ถํธํจ |
| SQLModel | FastAPI ์ ์์(Sebastian Ramirez)๊ฐ ๋ง๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ. ๋ด๋ถ์ ์ผ๋ก SQLAlchemy + Pydantic ๋ํผ |
| Peewee | ๊ฒฝ๋ ORM. ์๊ท๋ชจ ํ๋ก์ ํธ์ ์ ํฉ |
| Tortoise ORM | async ๋ค์ดํฐ๋ธ ORM. asyncio ๊ธฐ๋ฐ ํ๋ก์ ํธ์์ ์ฌ์ฉ |
Django๋ฅผ ์ฐ๋ฉด Django ORM, ๊ทธ ์ธ์๋ SQLAlchemy๊ฐ ๊ธฐ๋ณธ ์ ํ์ด๋ค. SQLModel๋ ๊ฒฐ๊ตญ SQLAlchemy๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ฏ๋ก, SQLAlchemy๋ฅผ ์ดํดํ๋ฉด SQLModel๋ ์์ฐ์ค๋ฝ๊ฒ ์ธ ์ ์๋ค.
3. ์ค์น
# SQLAlchemy๋ง ์ค์น
pip install sqlalchemy
# uv ์ฌ์ฉ ์
uv add sqlalchemy
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ผ์ด๋ฒ๋ ํจ๊ป ์ค์น (PostgreSQL ์์)
pip install sqlalchemy psycopg2-binary
# ๋น๋๊ธฐ ๋๋ผ์ด๋ฒ (asyncio ์ฌ์ฉ ์)
pip install sqlalchemy asyncpg4. ๋ ๊ฐ์ ๋ ์ด์ด
SQLAlchemy๋ Core์ ORM ๋ ๊ณ์ธต์ผ๋ก ๋๋๋ค.
โโโโโโโโโโโโโโโโโโโโโโโ
โ ORM โ โ ํด๋์ค๋ก ํ
์ด๋ธ ๋งคํ, ์ธ์
๊ด๋ฆฌ
โโโโโโโโโโโโโโโโโโโโโโโค
โ Core โ โ SQL ํํ์, ์์ง, ์ปค๋ฅ์
ํ
โโโโโโโโโโโโโโโโโโโโโโโค
โ DBAPI โ โ psycopg2, sqlite3 ๋ฑ ๋๋ผ์ด๋ฒ
โโโโโโโโโโโโโโโโโโโโโโโ
- Core: SQL์ Python ํํ์์ผ๋ก ์์ฑํ๋ ๋ ์ด์ด.
select(),insert()๊ฐ์ ํจ์๋ฅผ ์ฌ์ฉํ๋ค. - ORM: Core ์์์ Python ํด๋์ค์ ํ ์ด๋ธ์ ๋งคํํ๋ ๋ ์ด์ด. ๋๋ถ๋ถ ์ด ORM ๋ ์ด์ด๋ฅผ ์ฌ์ฉํ๋ค.
๋ ๋ ์ด์ด๋ฅผ ์์ด ์ธ ์ ์๋ค๋ ์ ์ด SQLAlchemy์ ๊ฐ์ ์ด๋ค. ORM์ด ๋ถํธํ ๋ณต์กํ ์ฟผ๋ฆฌ๋ Core๋ก ์ง์ ์์ฑํ ์ ์๋ค.
5. ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ (2.0 ์คํ์ผ)
SQLAlchemy 2.0๋ถํฐ select() ๊ธฐ๋ฐ ์ฟผ๋ฆฌ๊ฐ ํ์ค์ด๋ค. ์ด์ 1.x์ session.query() ๋ฐฉ์๋ ๋์ํ์ง๋ง ์ ํ๋ก์ ํธ์์๋ 2.0 ์คํ์ผ์ ๊ถ์ฅํ๋ค.
์์ง ์์ฑ
from sqlalchemy import create_engine
# SQLite
engine = create_engine("sqlite:///app.db")
# PostgreSQL
engine = create_engine("postgresql://user:password@localhost:5432/mydb")๋ชจ๋ธ ์ ์
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
age: Mapped[int | None] # nullable ์ปฌ๋ผ2.0์ ํ์ ํํธ ์คํ์ผ
Mapped[int]์ฒ๋ผ Python ํ์ ํํธ๋ก ์ปฌ๋ผ ํ์ ์ ์ ์ธํ๋ค. 1.x์Column(Integer)๋ฐฉ์๋ณด๋ค IDE ์ง์์ด ์ข๊ณ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํ๋ค.
ํ ์ด๋ธ ์์ฑ
Base.metadata.create_all(engine)CRUD ๊ธฐ๋ณธ
from sqlalchemy.orm import Session
from sqlalchemy import select
# Create
with Session(engine) as session:
user = User(name="ํ๊ธธ๋", email="hong@example.com", age=30)
session.add(user)
session.commit()
# Read
with Session(engine) as session:
stmt = select(User).where(User.age > 20)
users = session.scalars(stmt).all()
# Update
with Session(engine) as session:
stmt = select(User).where(User.name == "ํ๊ธธ๋")
user = session.scalars(stmt).first()
user.age = 31
session.commit()
# Delete
with Session(engine) as session:
stmt = select(User).where(User.name == "ํ๊ธธ๋")
user = session.scalars(stmt).first()
session.delete(user)
session.commit()6. ๊ด๊ณ(Relationship) ์ค์
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="posts")
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
posts: Mapped[list["Post"]] = relationship(back_populates="author")# ์ฌ์ฉ ์์
with Session(engine) as session:
user = session.scalars(select(User).where(User.name == "ํ๊ธธ๋")).first()
for post in user.posts:
print(post.title)7. Alembic์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
Alembic์ SQLAlchemy์ ๊ณต์ ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ๋ค. ๋ชจ๋ธ์ด ๋ณ๊ฒฝ๋๋ฉด Alembic์ด DDL(ALTER TABLE ๋ฑ)์ ์๋ ์์ฑํ๋ค.
# ์ค์น
pip install alembic
# ์ด๊ธฐํ
alembic init alembic
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์๋ ์์ฑ
alembic revision --autogenerate -m "add users table"
# ๋ง์ด๊ทธ๋ ์ด์
์ ์ฉ
alembic upgrade head
# ๋กค๋ฐฑ
alembic downgrade -1autogenerate ์ฃผ์์ฌํญ
--autogenerate๋ ๋ชจ๋ธ๊ณผ ์ค์ DB ์คํค๋ง๋ฅผ ๋น๊ตํด์ ์ฐจ์ด๋ฅผ ๊ฐ์งํ๋ค- ์ปฌ๋ผ ์ด๋ฆ ๋ณ๊ฒฝ์ ์๋ ๊ฐ์ง๊ฐ ์ ๋จ (์ญ์ + ์์ฑ์ผ๋ก ์ธ์)
- ์์ฑ๋ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ๋ฐ๋์ ๊ฒํ ํ ์ ์ฉํ ๊ฒ
8. ์์ฃผ ์ฐ๋ ์ฟผ๋ฆฌ ํจํด
from sqlalchemy import select, func, or_, desc
# ์ฌ๋ฌ ์กฐ๊ฑด (AND)
stmt = select(User).where(User.age > 20, User.name.like("%ํ%"))
# OR ์กฐ๊ฑด
stmt = select(User).where(or_(User.age > 30, User.name == "ํ๊ธธ๋"))
# ์ ๋ ฌ
stmt = select(User).order_by(desc(User.age))
# ํ์ด์ง๋ค์ด์
stmt = select(User).offset(10).limit(20)
# ์ง๊ณ
stmt = select(func.count()).select_from(User)
count = session.scalar(stmt)
# JOIN
stmt = select(User, Post).join(Post, User.id == Post.user_id)