새소식

이것저것 개발노트

[FastAPI] [Chapter 11] 데이터베이스 연결과 DB 모델

  • -

이번 포스팅에서는 데이터베이스로 MySQL의 Docker 컨테이너를 설정해, ToDo 앱에서 데이터베이스에 접속하는 방법을 알아본다.

 

Docker 컨테이너로 MySQL을 실행해 앱에서 접속해 본다.

 

demo-app과 함께 demo라는 이름의 데이터베이스를 가진 db 서비스를 추가한다. 

docker-compose.yaml

services: 
  demo-app:
    build: .
    volumes:
      - .dockervenv:/src/.venv
      - .:/src 
    ports:
      - 8000:8000                       # 호스트 머신의 8000번 포트를 docker의 8000번 포트에 연결 
    environment:
      - WATCHFILES_FORCE_POLLING=true   # 환경에 따라 핫 리로드를 위해 필요함 
  
  db:
    image: mysql:8.0
    platform: linux/x86_64
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      MYSQL_DATABASE: 'demo'
      TZ: 'Asia/Seoul'
    volumes:
      - mysql_data:/var/lib/mysql        # mysql_data라는 볼륨을 MySQL 데이터베이스 경로에 마운트
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - 33306:3306
  
volumes:
  mysql_data:                           # 이 부분은 전역적으로 정의된 볼륨을 의미

 

 

 

ToDo 앱과 MySQL이 다음과 같이 동시에 실행된다.

> sudo docker compose up

 

컨테이너 내 MySQL 데이터베이스에 접근할 수 있는지 확인해보자.

docker compose up이 실행된 상태에서 다른 터미널을 열고, 프로젝트 디렉터리에서 docker compose exec db mysql demo를 실행한다.

다음처럼 MySQL 클라이언트가 실행되고 DB에 접속된 것을 확인할 수 있다.

> sudo docker compose exec db mysql demo

 

 

 

 

FastAPI 앱에서 MySQL에 접속하기 위한 준비를 해보자.

 

MySQL 클라이언트 설치 

FastAPI에서는 MySQL과의 연결을 위해 sqlalchemy라는 ORM (Object-Relational Mapper) 라이브러리를 사용한다. 

ORM은 객체지향 프로그래밍과 데이터베이스 간의 연결을 쉽게 해주는 기술이다. 

이를 통해 데이터베이스의 데이터를 객체로 다루고, 객체를 데이터베이스에 저장하거나 조회할 수 있다.

sqlalchemy는 파이썬에서는 상당히 대중적인 라이브러리로, Flask 등 다른 웹 프레임워크에서도 사용된다. 

 

ORM이란?

ORM은 파이썬 객체를 MySQL과 같은 관계형 데이터베이스 (RDBMS)의 데이터 구조로 변환한다.

MySQL의 경우 테이블 구조를 클래스로 정의하면 이를 읽거나 저장하는 SQL문을 발행해 준다.

FastAPI에서는 Peewee라는 ORM도 지원한다. 

 

sqlalchemy는 백엔드에 다양한 데이터베이스를 이용할 수 있다.

이번에 MySQL 클라이언트로 pymysql도 함께 설치한다.

demo-app이 실행된 상태에서 poetry add를 실행하여 두 의존성 패키지를 설치한다.

> sudo docker compose exec demo-app poetry add sqlalchemy pymysql

 

설치하면 pyproject.toml과 poetry.lock의 내용의 변경된 것을 확인할 수 있다. 

pyproject.toml

[tool.poetry]
name = "demo-app"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.115.2"
uvicorn = {extras = ["standard"], version = "^0.32.0"}
sqlalchemy = "^2.0.36"
pymysql = "^1.1.1"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

 

api/db.py를 추가한다.

api/db.py 

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

DB_URL = "mysql+pymysql://root@db:3306/demo?charset=utf8"

db_engine = create_engine(DB_URL, echo=True)
db_session = sessionmaker(autocommit=False, autoflush=False, bind=db_engine)

Base = declarative_base()

def get_db():
    with db_session() as session:
        yield session

 

DB_URL에 정의한 MySQL의 Docker 컨테이너에 접속할 세션을 생성

라우터에서는 get_db() 함수로 이 세션을 가져와 DB에 접근할 수 있도록 한다. 

 

 

FastAPI에 DB 모델을 정의한다.

 

ToDo 앱을 위해 아래 2개의 표를 정의한다.

 

tasks 테이블 정의

컬럼명 Type 비고
id INT primary, auto increment
title VARCHAR (1024)   

 

dones 테이블 정의 

컬럼명 Type 비고
id INT primary, auto increment, foreign key (task.id)

 

 

Tasks의 레코드는 작업 하나하나에 대응하며, dones는 Tasks 중 완료된 작업만 해당 Task와 동일한 id의 레코드를 가진다. 여기서 Tasks의 id와 dones의 id는 1:1 매핑으로 설정되어 있다. 

보통 1:1 매핑의 경우 정구화 측면에서 하나의 테이블로 하는 경우가 많지만, 해당 책에서는 Task와 done의 리소스를 명확하게 분리하여, 이해하기 쉽도록 별도의 테이블로 정의하였다. 

 

api/models/task.py를 작성하자

api/models/task.py

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship 

from api.db import Base 

class Task(Base):
    __tablename__ = "tasks"
    
    # Column은 테이블의 각 컬럼을 나탄냄 
    id = Column(Integer, primary_key=True)
    title = Column(String(1024))
    
    # 테이블간의 관계를 정의 : Task 객체에서 Done 객체를 참조하거나 그 반대도 가능 
    done = relationship("Done", back_populates="task", cascade="delete")    
    
class Done(Base):
    __tablename__ = "dones"
    
    id = Column(Integer, ForeignKey("tasks.id"), primary_key=True)
    
    task = relationship("Task", back_populates="done")

 

Column은 테이블의 각 컬럼을 나타낸다.

첫번째 인수는 컬럼의 타입을 전달한다. (Integer 또는 String)

그리고 두번째 인수 이후에 컬럼의 설정을 작성한다.

위의 primary_key=True나 ForeignKey("tasks.id") 외에도, Null 제약 조건 (nullable=False) 등 지원

 

relationship은 테이블 (모델 클래스) 간의 관계를 정의한다.

이를 통해 Task 객체에서 Done 객체를 참조하거나 그 반대도 가능

 

cascade="delete"를 지정하면

DELETE /tasks/{task_id} 인터페이스에서 Task를 삭제할 때, 외부 키에 지정된 동일한 id의 done이 있으면 자동으로 삭제된다.

 

작성한 ORM 모델을 바탕으로 DB에 테이블을 생성하고, DB 마이그레이션 용 스크립트를 작성한다. 

api/migrate_db.py

from sqlalchemy import create_engine

from api.models.task import Base

DB_URL = "mysql+pymysql://root@db:3306/demo?charset=utf8"
engine = create_engine(DB_URL, echo=True)


def reset_database():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    
if __name__=="__main__":
    reset_database()

 

 

아래 스크립트를 실행하여 Docker 컨테이너의 MySQL에 테이블을 작성한다.

이미 같은 이름의 테이블이 있는 경우 삭제한 후 재작성된다.

> sudo docker compose exec demo-app poetry run python -m api.migrate_db

 

 

이것으로 DB에 테이블이 생성되었다.

 

실제로 테이블이 생성되었는지 확인해보자. 

docker compose up으로 컨테이너가 실행된 상태에서 MySQL 클라이언트를 실행한다. 

> sudo docker compose exec db mysql demo

 

 

다음 포스팅에서 DB의 쓰기와 읽기 처리를 작성하고 

API와 연결해본다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.