SQLAlchemy는 Python에서 관계형 데이터베이스를 편리하게 사용할 수 있도록 도와주는 라이브러리입니다.
주요 기능:
ORM(Object-Relational Mapping) 은 관계형 데이터베이스의 테이블을 Python 클래스(객체)처럼 다룰 수 있게 해주는 기술입니다.
| 데이터베이스 개념 | Python 개념 |
|---|---|
| 테이블 (Table) | 클래스 (Class) |
| 행/레코드 (Row) | 객체 (Object) |
| 컬럼 (Column) | 속성 (Attribute) |
예시:
데이터베이스 테이블:
| id | username |
|---|---|
| 1 | qu3vipon |
Python ORM 코드:
user = User(id=1, username='qu3vipon')
즉, SQL을 직접 쓰지 않고 Python 코드로 DB를 조작할 수 있게 됩니다.
로컬에 MySQL을 직접 설치하지 않고, Docker를 이용해 간편하게 실행합니다.
# MySQL 컨테이너 실행
docker run -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=todos \
-e MYSQL_DATABASE=todos \
-d \
-v todos:/db \
--name todos \
mysql:8.0
# 실행 중인 컨테이너 확인
docker ps
# 컨테이너 로그 확인
docker logs todos
# 볼륨 목록 확인
docker volume ls
명령어 옵션 설명:
-p 3306:3306 : 로컬 포트 3306 ↔ 컨테이너 포트 3306 연결-e MYSQL_ROOT_PASSWORD=todos : root 비밀번호 설정-e MYSQL_DATABASE=todos : 기본 DB 이름 설정-d : 백그라운드 실행-v todos:/db : 데이터 영속성을 위한 볼륨 마운트--name todos : 컨테이너 이름 지정# MySQL 컨테이너 내부 접속
docker exec -it todos bash
# MySQL 로그인
mysql -u root -p
-- 데이터베이스 목록 확인
SHOW databases;
-- todos DB 선택
USE todos;
-- todo 테이블 생성
CREATE TABLE todo(
id INT NOT NULL AUTO_INCREMENT,
contents VARCHAR(256) NOT NULL,
is_done BOOLEAN NOT NULL,
PRIMARY KEY (id)
);
-- 샘플 데이터 삽입
INSERT INTO todo (contents, is_done) VALUES ("FastAPI Section 0", true);
-- 데이터 조회
SELECT * FROM todo;
pip install SQLAlchemy # ORM 라이브러리
pip install pyMySQL # Python ↔ MySQL 연결 드라이버
pip install cryptography # MySQL 인증/암호화 처리
database/connection.pyfrom sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# DB 연결 주소 (형식: mysql+pymysql://유저:비밀번호@호스트:포트/DB명)
DATABASE_URL = "mysql+pymysql://root:todos@127.0.0.1:3306/todos"
# 엔진 생성 (실제 DB와 연결하는 핵심 객체, echo=True는 실행된 SQL을 콘솔에 출력)
engine = create_engine(DATABASE_URL, echo=True)
# 세션 팩토리 생성 (DB와 대화하는 세션을 만드는 공장)
# autocommit=False : 명시적으로 commit() 해야 DB에 반영됨
# autoflush=False : 명시적으로 flush() 하기 전까지 쿼리를 DB에 보내지 않음
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
>>> from database.connection import SessionFactory
>>> session = SessionFactory()
>>> from sqlalchemy import select
>>> session.scalar(select(1)) # 1이 반환되면 연결 성공!
database/orm.pyfrom sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base() # 모든 ORM 모델의 부모 클래스
class ToDo(Base):
__tablename__ = "todo" # 연결할 실제 DB 테이블 이름
# 컬럼 정의 (DB 테이블의 각 컬럼과 1:1 매핑)
id = Column(Integer, primary_key=True, index=True)
contents = Column(String(256), nullable=False)
is_done = Column(Boolean, nullable=False)
def __repr__(self):
# 객체를 출력할 때 보여줄 문자열 형식
return f"ToDo(id={self.id}, contents={self.contents}, is_done={self.is_done})"
>>> from database.connection import SessionFactory
>>> session = SessionFactory()
>>> from sqlalchemy import select
>>> from database.orm import ToDo
# 모든 ToDo 조회
>>> todos = list(session.scalars(select(ToDo)))
>>> todos
# 하나씩 출력
>>> for todo in todos:
... print(todo)
scalar vs scalars 차이SQLAlchemy에서 헷갈리기 쉬운 두 메서드의 차이입니다.
# scalars() - 여러 행을 ORM 객체 리스트로 반환
session.scalars(select(ToDo))
# → [ToDo(id=1, ...), ToDo(id=2, ...), ...]
# scalar() - 단일 행 하나만 반환
session.scalar(select(ToDo).where(ToDo.id == 1))
# → ToDo(id=1, ...) 객체 하나
쿼리 결과는 원래 행/열의 2차원 테이블 형태인데, 첫 번째 열만 뽑아서 단순하게(scalar) 만들어줍니다.
덕분에Row객체가 아니라 바로ToDo객체로 받을 수 있습니다.
FastAPI의 Depends를 이용해 각 API 핸들러에 DB 세션을 자동으로 주입합니다.
# database/connection.py 에 추가
from sqlalchemy.orm import Session
def get_db():
# 요청마다 새 세션을 열고, 요청이 끝나면 자동으로 닫아줌
db = SessionFactory()
try:
yield db
finally:
db.close()
API가 반환할 데이터 형식을 Pydantic으로 정의합니다.
# schema/response.py
from pydantic import BaseModel
from typing import List
class ToDoSchema(BaseModel):
id: int
contents: str
is_done: bool
class Config:
from_attributes = True # ORM 객체 → Pydantic 변환 허용 (구버전: orm_mode = True)
class ListToDoResponse(BaseModel):
todos: List[ToDoSchema]
# database/repository.py
from typing import List
from sqlalchemy import select
from sqlalchemy.orm import Session
from database.orm import ToDo
def get_todos(session: Session) -> List[ToDo]:
return list(session.scalars(select(ToDo)))
# main.py
@app.get("/todos", status_code=200)
def get_todos_handler(
order: str | None = None,
session: Session = Depends(get_db), # DB 세션 자동 주입
) -> ListToDoResponse:
todos: List[ToDo] = get_todos(session=session)
if order and order == "DESC":
return ListToDoResponse(
todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
)
return ListToDoResponse(
todos=[ToDoSchema.from_orm(todo) for todo in todos]
)
ToDoSchema.from_orm(todo): ORM 객체(ToDo)를 Pydantic 스키마(ToDoSchema)로 변환합니다.
# database/repository.py 에 추가
def get_todo_by_todo_id(session: Session, todo_id: int) -> ToDo | None:
# where 조건으로 특정 id를 가진 행 하나만 반환
return session.scalar(select(ToDo).where(ToDo.id == todo_id))
# main.py
@app.get("/todos/{todo_id}", status_code=200)
def get_todo_handler(
todo_id: int,
session: Session = Depends(get_db),
) -> ToDoSchema:
todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
if todo:
return ToDoSchema.from_orm(todo)
raise HTTPException(status_code=404, detail="ToDo Not Found")
# schema/request.py
from pydantic import BaseModel
class CreateToDoRequest(BaseModel):
contents: str
is_done: bool
# id는 DB가 AUTO_INCREMENT로 자동 생성하므로 요청에 포함하지 않음
# database/repository.py 에 추가
def create_todo(session: Session, todo: ToDo) -> ToDo:
session.add(instance=todo) # 세션에 객체 추가 (아직 DB에 저장 안 됨)
session.commit() # DB에 실제로 저장 (INSERT 실행)
session.refresh(instance=todo) # DB에서 최신 상태 다시 읽어오기 (자동 생성된 id 반영)
return todo
# main.py
@app.post("/todos", status_code=201)
def create_todo_handler(
request: CreateToDoRequest,
session: Session = Depends(get_db),
) -> ToDoSchema:
todo: ToDo = ToDo.create(request=request) # ORM 객체 생성 (id=None 상태)
todo: ToDo = create_todo(session=session, todo=todo) # DB 저장 후 id 할당됨
return ToDoSchema.from_orm(todo)
session.refresh()가 중요한 이유: DB에 저장 후 AUTO_INCREMENT로 생성된id값을 Python 객체에 반영하기 위해 필요합니다.
# database/repository.py 에 추가
def update_todo(session: Session, todo: ToDo) -> ToDo:
session.add(instance=todo) # 변경된 객체를 세션에 반영
session.commit() # DB에 UPDATE 실행
session.refresh(instance=todo) # 최신 상태 다시 읽기
return todo
# main.py
@app.patch("/todos/{todo_id}", status_code=200)
def update_todo_handler(
todo_id: int,
is_done: bool = Body(..., embed=True),
session: Session = Depends(get_db),
):
todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
if todo:
# is_done 값에 따라 done() 또는 undone() 메서드 호출
todo.done() if is_done else todo.undone()
todo: ToDo = update_todo(session=session, todo=todo)
return ToDoSchema.from_orm(todo)
raise HTTPException(status_code=404, detail="ToDo Not Found")
# database/repository.py 에 추가
from sqlalchemy import delete
def delete_todo(session: Session, todo_id: int) -> None:
# DELETE FROM todo WHERE id = todo_id 실행
session.execute(delete(ToDo).where(ToDo.id == todo_id))
session.commit()
# main.py
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo_handler(
todo_id: int,
session: Session = Depends(get_db),
):
todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
if not todo:
raise HTTPException(status_code=404, detail="ToDo Not Found")
delete_todo(session=session, todo_id=todo_id)
# 204 No Content: 성공했지만 반환할 데이터 없음 → return 생략
코드가 길어지면 파일을 분리해서 관리하는 게 중요합니다.
클라이언트 요청
↓
FastAPI 라우터 (main.py)
↓
Repository 함수 (database/repository.py) ← DB 쿼리 로직 분리
↓
ORM 모델 (database/orm.py) ← 테이블 구조 정의
↓
SQLAlchemy Engine (database/connection.py) ← 실제 DB 연결
↓
MySQL (Docker)