RDB

데이터를 저장할 때 가장 많이 사용하는 시스템이 DBMS이다. 특히 전통적으로 RDB를 많이 사용하고 있다. RDB(Relational Database)란 관계형 데이터 모델에 기초를 둔 데이타베이스이다. 관계형 데이터 모델은 key와 value들의 관계를 2차원의 테이블 형태로 표현한다. RDBMS(Relational Database Management System)는 관계형 데이터베이스를 생성하고 수정하고 관리할 수 있는 소프트웨어이다.

요새는 백엔드에서 DB에 쿼리를 실행할 때 ORM(Object Relation Mapper)이란 일종의 중계 프로그램을 사용한다. 말 그대로 객체와 관계를 매핑해주는 프로그램이라 생각하면 된다. 파이썬에서 가장 유명한 ORM 중 하나인 SQLAlchemy를 이용해 MySQL과 연결해 보자.

MySQL 도커 컨테이너 띄우기

  • --name: 컨테이너 이름
  • -e: 환경 변수
  • -d: 백그라운드 모드(Detached mode)
  • mysql:8.0: 이미지 이름
  • –character-set-server=utf8mb4: (MySQL 설정) utf8은 3바이트인데 요즘 이모지를 많이 사용하니깐 mb4로 4 바이트 까지 확장 가능하도록 설정
  • –collation-server=utf8mb4_unicode_ci: (MySQL 설정) 정렬할 char는 utf8mb4 형태이고 ci(character insensivity) 로 대소문자 구분 안하도록 설정
(fastapi) PS D:\workspace\fastapi> docker run --name fastapi-mysql -e MYSQL_ROOT_PASSWORD=1234 -d mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

이제 만든 컨테이너를 실행시켜보자.

  • -it: interactive tty로 컨테이너와 입출력을 받고, 터미널과 비슷한 환경을 조성해 준다. 컨테이너 내부로 진입하도록 attach 가 가능한 상태로 설정
  • mysql: mysql을 실행
  • uroot: (MySQL 명령어) user root로 root 계정으로 들어가는 설정
  • -p: 비밀번호를 입력하겠다는 옵션
(fastapi) PS D:\workspace\fastapi> docker exec -it fastapi-mysql mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.30 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)
mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A    

Database changed
mysql> select user, host from user;
+------------------+-----------+
| user             | host      |
+------------------+-----------+
| root             | %         |
| mysql.infoschema | localhost |
| mysql.session    | localhost |
| mysql.sys        | localhost |
| root             | localhost |
+------------------+-----------+
5 rows in set (0.00 sec)

SQLAlchemy로 MySQL과 연결

  • main.py: FastAPI 작성 파일
  • database.py: SQLAlchemy 설정
  • models.py: SQLAlchemy Models (테이블 표현)
  • schemas.py: Pydantic Models

MySQL 도커 컨테이너 실행

$ docker run -d --name fastapi-db \
    -p 3306:3306 \
    -e MYSQL_ROOT_PASSWORD=1234 \
    -e MYSQL_DATABASE=dev \
    -e MYSQL_USER=admin \
    -e MYSQL_PASSWORD=1234 \
    mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
(fastapi) PS D:\workspace\fastapi> docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS
             NAMES
c1b056bb7674   mysql:8.0   "docker-entrypoint.s…"   53 seconds ago   Up 52 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   fastapi-db

PyMySQL, SQLAlchemy 설치

PyMySQL (MySQL과 파이썬을 연결하기 위한 라이브러리) 설치

(fastapi) PS D:\workspace\fastapi> pip install PyMySQL

SQLAlchemy 설치

(fastapi) PS D:\workspace\fastapi> pip install sqlalchemy

database.py

  1. create_engine 함수를 이용해 DB 연결할 엔진 생성
    • 데이터베이스의 주소를 입력으로 받음
    • dialect+driver://username:password@host:port/database
  2. sessionmaker 함수를 이용해 세션 생성
    • 만든 엔진의 세션을 생성
  3. declarative_base 함수로 모델 정의를 위한 부모 클래스 생성
    • ORM을 사용할 때 처리할 데이터베이스의 테이블을 설명하고 해당 테이블에 매핑(Mapping) 될 클래스를 정의하는 작업이 필요
    • ORM에게 다룰 테이블을 알려줌과 동시에 데이터베이스의 테이블에 매핑될 테이블 클래스들을 코드로 구현
    • 데이터베이스의 구조와 코드를 연결
    • 데이터베이스에 user라는 테이블이 존재하면 코드 상에도 이 테이블과 정상적으로 연결 될 수 있도록 user 테이블을 코드로 구현함
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker


engine = create_engine("mysql+pymysql://admin:1234@0.0.0.0:3306/dev")
SessionLocal = sessionmaker(
    bind=engine,
    autocommit=False,
    autoflush=False,
)

Base = declarative_base()

models.py

DB의 테이블을 정의하는 파일이다. 클래스를 생성할 때는 __tablename__Primary key가 반드시 필요하다.

from sqlalchemy import Boolean, Column, Integer, String

from .database import Base


# Mapping 클래스 생성
class User(Base):  # 식별하기 쉽게 테이블명으로 지정
    __tablename__ = "user"  # 테이블명

    id = Column(Integer, primary_key=True)
    email = Column(String(255), unique=True, index=True)
    password = Column(String(255))
    is_active = Column(Boolean, default=True)

schemas.py

Pydantic 모델을 정의한 파일이다. SQLAlchemy와 Pydantic 모두 “모델”이란 용어를 사용하는데 FastAPI 공식 문서에는 pydantic “model”을 “schema”로 표현했다.

from pydantic import BaseModel


class UserBase(BaseModel):
    email: str


class UserCreate(UserBase):
    password: str


class User(UserBase):
    id: int
    email: str
    is_active: bool

    # pydantic이 SQLAlchemy 모델을 사용할 수 있게 함
    class Config:
        orm_mode = True

main.py

FastAPI를 실행할 메인 파일이다.

from typing import List

from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session

from . import models, schemas
from .database import SessionLocal, engine

# DB에 model.py에서 정의한 테이블을 생성 
models.Base.metadata.create_all(bind=engine)


app = FastAPI()


# 의존성 주입을 사용하기 위한 함수
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# 유저를 생성하는 API 
@app.post("/users", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    # filter_by라는 메서드로 User 테이블에 user.email이라는 이메일이 존재하는지 확인
    existed_user = db.query(models.User).filter_by(
        email=user.email
    ).first()

    if existed_user:
        raise HTTPException(status_code=400, detail="Email already registered")

    # 입력받은 email, password로 유저 생성 
    user = models.User(email=user.email, password=user.password)
    db.add(user)  # DB에 insert
    db.commit()  # DB에 반영 (SessionLocal에서 autocommit을 false로 했기 때문)

    return user


# 유저를 읽는 API
@app.get("/users", response_model=List[schemas.User])
def read_users(db: Session = Depends(get_db)):
    # db에서 쿼리를 날려서 User table에 있는 모든 레코드를 가져옴 
    return db.query(models.User).all()

API 실행

(fastapi) PS D:\workspace\fastapi> uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload