๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Back-end & Server/FastAPI

[FastAPI] Relational Database

728x90
๋ฐ˜์‘ํ˜•

 

๐Ÿฐ FastAPI ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด์„œ ๊ฐœ์ธ์ ์œผ๋กœ ์ •๋ฆฌํ•œ ๊ธ€ ์ž…๋‹ˆ๋‹ค.

 

๋จผ์ € DB๋ฅผ ์ค€๋น„ํ•œ๋‹ค. ์ด ํฌ์ŠคํŠธ์—์„œ๋Š” MariaDB๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 

DB์˜ ๋‚ด์šฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • DB๋ช… : test
  • Table ๋ช… : User
idUser username password nickname
1 admin1 SHA2(admin1,256) Admin1
2 admin2 SHA2(admin2,256) Admin2
3 pupba12 SHA2(qwer1234,256) Pupba

 

 

ORM(Object-Relational Mapping)

FastAPI๋Š” ORM์„ SQLAlchemy๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„ํ•œ๋‹ค.

pip install sqlalchemy

 

๋งŒ๋“  ORM ๋ชจ๋“ˆ์˜ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

1. SQLAlchemy - database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
import json
path = "./sql_app/secret.json"
se = json.loads(open(path).read())
R_SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{se.get('S_USER')}:{se.get('S_PW')}@{se.get('HOST')}:{se.get('PORT')}/{se.get('DB')}?charset=utf8"
CUD_SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{se.get('M_USER')}:{se.get('M_PW')}@{se.get('HOST')}:{se.get('PORT')}/{se.get('DB')}?charset=utf8"


r_engine = create_engine(R_SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
cud_engine = create_engine(CUD_SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
r_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=r_engine)
cud_SessionLocal = sessionmaker(
    autocommit=False, autoflush=False, bind=cud_engine)

Base = declarative_base()

 

create_engine(url, **kwargs)

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๋Š” ์—”์ง„์„ ์ƒ์„ฑ
  • url : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์—ฐ๊ฒฐ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” URL
  • **kwargs : ์ถ”๊ฐ€์ ์ธ ์„ค์ •์„ ์œ„ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜(ํ’€๋ง, ๋ฌธ์ž์ธ์ฝ”๋”ฉ ๋“ฑ)

 

sessionmaker(class_=None,bind=None,autoflush=True,autocommit=False,expire_on_commit=True,**kwargs)

  • ์„ธ์…˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜, ์„ธ์…˜์€ SQLAlchemy์—์„œ DB์™€์˜ ํŠธ๋žœ์žญ์…˜ ๋‹จ์œ„๋กœ ์ž‘์—…ํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ๊ฐ์ฒด
  • class_ : ์ƒ์„ฑ๋  ์„ธ์…˜ ํด๋ž˜์Šค์˜ ๋ถ€๋ชจ ํด๋ž˜์Šค
  • bind : ์—ฐ๊ฒฐ ์—”์ง„์„ ์ง€์ •ํ•˜์—ฌ ์„ธ์…˜๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋ฐ”์ธ๋”ฉ
  • autoflush : ์„ธ์…˜์—์„œ ๋ณ€๊ฒฝ๋œ ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ˜์˜ํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •
  • autocommit : ์„ธ์…˜์ด ์ž๋™์œผ๋กœ ์ปค๋ฐ‹ํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •
  • expire_on_commit : ์ปค๋ฐ‹ ํ›„์— ์„ธ์…˜์—์„œ ๋กœ๋“œ๋œ ๊ฐ์ฒด๋ฅผ ๋งŒ๋ฃŒ(expire)ํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •
  • **kwargs : ์ถ”๊ฐ€์ ์ธ ์„ค์ •์„ ์œ„ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜

 

declarative_base(cls=object,name='Base',metadata=None,mapper=None,class_registry=None,metaclass=DeclarativeMeta)

  • ORM์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ ํŒŒ์ด์ฌ ํด๋ž˜์Šค๋ฅผ ๋งคํ•‘ํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ํด๋ž˜์Šค ์ƒ์„ฑ
  • cls : ์ƒ์„ฑ๋  ๊ธฐ๋ณธ ํด๋ž˜์Šค์˜ ๋ถ€๋ชจ ํด๋ž˜์Šค
  • name : ์ƒ์„ฑ๋  ๊ธฐ๋ณธ ํด๋ž˜์Šค์˜ ์ด๋ฆ„
  • metadata : ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ์ง€์ •ํ•˜์—ฌ ํ…Œ์ด๋ธ” ์ƒ์„ฑ, ๋ณ€๊ฒฝ ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Œ
  • mapper : ์‚ฌ์šฉ์ž ์ •์˜ ๋งคํผ๋ฅผ ์ง€์ •ํ•˜์—ฌ ํด๋ž˜์Šค์™€ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ๋งคํ•‘์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Œ
  • class_registry : ํด๋ž˜์Šค ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฐ์ฒด๋ฅผ ์ง€์ •ํ•˜์—ฌ ํด๋ž˜์Šค๋“ค์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Œ
  • metaclass : ์ƒ์„ฑ๋œ ํด๋ž˜์Šค์˜ ๋ฉ”ํƒ€ํด๋ž˜์Šค๋ฅผ ์ง€์ •

 

 

2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ ์ƒ์„ฑ - models.py

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ SQLAlchemy ๋ชจ๋ธ ์ƒ์„ฑ์„ ์œ„ํ•ด database.py์—์„œ ์ƒ์„ฑํ•œ Base ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ ธ์™€ ์ƒ์†ํ•˜์—ฌ ๋ชจ๋ธ์„ ์ •์˜ํ•œ๋‹ค.

# Table type
from sqlalchemy import Column, Integer, VARCHAR
# ํ…Œ์ด๋ธ” ๊ด€๊ณ„๋ฅผ ์œ„ํ•œ import
# from sqlalchemy.orm import relationship
from sql_app.database import Base


class User(Base):
    """
    User Model
        - id(INT,PRI,UNIQ,NOT NULL): id
        - username(String,UNIQ,NOT NULL) : login id
        - password(String,UNIQ,NOT NULL) : login pw, save to SAH2 HashData
        - nickname(String,UNIQ,NOT NULL) : nickname
    Description : User Table
    """
    # SQLAlchemy์—๊ฒŒ ๊ฐ ๋ชจ๋ธ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ” ์ด๋ฆ„
    __tablename__ = "User"

    idUser = Column(Integer, primary_key=True, unique=True,
                    nullable=False, autoincrement=True)
    username = Column(VARCHAR(45), unique=True, nullable=False)
    password = Column(VARCHAR(64), nullable=False)
    nickname = Column(VARCHAR(45), unique=True, nullable=False)

    """
    - ํ…Œ์ด๋ธ”๊ฐ„ ๊ด€๊ณ„ ์„ค์ • -
    relationship(target, **kwargs)->target ๋ชจ๋ธ์„ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด
    - back_populates :  ๊ด€๊ณ„์˜ ๋ฐ˜๋Œ€์ชฝ์—์„œ ํ•ด๋‹น ๊ด€๊ณ„๋ฅผ ์—ญ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋Š” ์†์„ฑ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    - uselist : ๊ด€๊ณ„๊ฐ€ ๋ฆฌ์ŠคํŠธ(list) ํ˜•ํƒœ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ True์ด๋ฉฐ, False๋กœ ์„ค์ •ํ•˜๋ฉด ๊ด€๊ณ„๊ฐ€ ์Šค์นผ๋ผ(scalar) ํ˜•ํƒœ๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค.
    - lazy : ๊ด€๊ณ„์˜ ๋กœ๋”ฉ ์ „๋žต์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. select๋กœ ์„ค์ •ํ•˜๋ฉด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ๊ด€๋ จ๋œ ๊ฐ์ฒด๋ฅผ ๋กœ๋”ฉํ•˜๊ณ , joined๋กœ ์„ค์ •ํ•˜๋ฉด ์ฟผ๋ฆฌ ์‹œ ์กฐ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•œ ๋ฒˆ์— ๊ด€๋ จ๋œ ๊ฐ์ฒด๋ฅผ ๋กœ๋”ฉํ•ฉ๋‹ˆ๋‹ค.
    - cascade : ๊ด€๊ณ„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์—ฐ๊ด€๋œ ๊ฐ์ฒด์— ์–ด๋–ค ๋™์ž‘์„ ์ „ํŒŒํ• ์ง€ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, delete๋ฅผ ์„ค์ •ํ•˜๋ฉด ๋ถ€๋ชจ ๊ฐ์ฒด๊ฐ€ ์‚ญ์ œ๋  ๋•Œ ์—ฐ๊ด€๋œ ์ž์‹ ๊ฐ์ฒด๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.
    """

 

 

3. Pydantic ๋ชจ๋ธ  ๋งŒ๋“ค๊ธฐ - schemas.py

์ฝ๊ธฐ์šฉ ๋ชจ๋ธ์„ ์œ„ํ•ด pydantic ๋ชจ๋ธ์„ ๋งŒ๋“ ๋‹ค.

from typing import List, Union
from pydantic import BaseModel

# Only One User


class UserIn(BaseModel):
    """
    UserIn
        - id : int or None
        - passwd : str
        - username : str
        - nickname : str
    """
    idUser: Union[int, None] = None
    username: str
    password: str
    nickname: str

    class Config:
        from_attributes = True


class UserOut(BaseModel):
    """
    UserOut
        - id : int
        - username : str
        - nickname : str
    """
    idUser: int
    username: str
    nickname: str

    class Config:
        from_attributes = True
# Users


class UsersOut(BaseModel):
    """
    UsersOut
        - users : List[UserOut]
    """
    users: List[UserOut]

    class Config:
        from_attributes = True

 

์—ฌ๊ธฐ์„œ orm_mode๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ์˜ ํ•„๋“œ์™€ Pydantic ๋ชจ๋ธ์˜ ํ•„๋“œ๋ฅผ ์ผ์น˜์‹œ์ผœ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  ORM๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋Œ€ ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™˜ ๋ฐ ์ง๋ ฌํ™” ์ž‘์—…์„ ๋ณด๋‹ค ํŽธ๋ฆฌํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

4. CRUD ์œ ํ‹ธ๋ฆฌํ‹ฐ - crud.py

CRUD๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ "์ƒ์„ฑ, ์ฝ๊ธฐ, ์—…๋ฐ์ดํŠธ, ์‚ญ์ œ" ํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

from sqlalchemy.orm import Session
from sql_app import models, schemas
from hashlib import sha256

# Create


def createUser(db: Session, user: schemas.UserIn):
    sha2PW = sha256(user.password.encode()).hexdigest()
    db_user = models.User(
        idUser=user.idUser,
        username=user.username,
        password=sha2PW,
        nickname=user.nickname
    )
    db.add(db_user)  # ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜์— ์ถ”๊ฐ€
    db.commit()  # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋ฐ‹
    db.refresh(db_user)  # ์ƒ์„ฑ๋œ ID์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋„๋ก ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    return db_user

# Read


def getUser(db: Session, username: str):
    return db.query(models.User).filter(models.User.username == username).first()


def getUsers(db: Session):
    return db.query(models.User).offset(0).limit(100).all()

# Update


def updateUser(db: Session, target: models.User, updateUser: schemas.UserIn):
    # To Dict
    updatedData = updateUser.model_dump(exclude_unset=True)
    if updatedData['username'] != target.username:
        target.username = updatedData['username']
    if updatedData['password'] != target.password:
        target.password = updatedData['password']
    if updatedData['nickname'] != target.nickname:
        target.nicknmae = updatedData['nickname']

    db.commit()
    db.refresh(target)
    return target
# Delete


def deleteUser(db: Session, target: models.User):
    db.delete(target)
    db.commit()
    return target

 

 

5. ์‚ฌ์šฉ - main.py

from typing import Union, Dict
from sql_app import crud, models, schemas
from sql_app.database import r_SessionLocal, cud_SessionLocal, r_engine, cud_engine, R_SQLALCHEMY_DATABASE_URL, CUD_SQLALCHEMY_DATABASE_URL
from sqlalchemy.orm import Session
from fastapi import FastAPI, Depends, HTTPException, Form
from fastapi.middleware.cors import CORSMiddleware

models.Base.metadata.create_all(bind=r_engine)
models.Base.metadata.create_all(bind=cud_engine)

app = FastAPI()

# Dependency
app.add_middleware(
    CORSMiddleware,
    allow_origins=[R_SQLALCHEMY_DATABASE_URL, CUD_SQLALCHEMY_DATABASE_URL],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)


def get_rdb():
    db = r_SessionLocal()
    try:
        yield db
    finally:
        db.close()


def get_cuddb():
    db = cud_SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.post('/user/', response_model=schemas.UserOut, tags=['GET'])
async def get_user(username: str, db: Session = Depends(get_rdb)):
    result = crud.getUser(db, username=username)
    if result is None:
        raise HTTPException(status_code=404, detail="User not found")
    return result


@app.post('/users/', response_model=schemas.UsersOut, tags=['GET'])
async def get_users(db: Session = Depends(get_rdb)):
    results = crud.getUsers(db)
    return {"users": results}

forms = {
    'target': {'min_length': 5, 'max_length': 15, 'title': 'Target', 'description': "Change Target ID"},
    'username': {'min_length': 5, 'max_length': 15, 'title': 'ID', 'description': "Input ID"},
    'password': {'min_length': 8, 'max_length': 15, 'title': 'Password', 'description': "Input Password"},
    'nickname': {'min_length': 2, 'max_length': 12, 'title': 'NickName', 'description': "Input Nickname"}
}


@app.post('/signin/', response_model=Union[schemas.UserOut, Dict[str, str]], tags=['Create'])
async def signin(
        username: str = Form(..., **forms['username']),
        password: str = Form(..., **forms['password']),
        nickname: str = Form(..., **forms['nickname']),
        rdb: Session = Depends(get_rdb),
        cuddb: Session = Depends(get_cuddb)):
    result = crud.getUser(rdb, username=username)
    if (result.username if result is not None else None) == username:
        return {'msg': "ID is Already...."}
    else:
        user = schemas.UserIn(
            username=username, password=password, nickname=nickname)
        result = crud.createUser(cuddb, user)
        return result


@app.post('/update/', response_model=Union[schemas.UserOut, Dict[str, str]], tags=['Update'])
async def update(
        target: str = Form(..., **forms['target']),
        username: str = Form(..., **forms['username']),
        password: str = Form(..., **forms['password']),
        nickname: str = Form(..., **forms['nickname']),
        rdb: Session = Depends(get_rdb),
        cuddb: Session = Depends(get_cuddb)):
    result = crud.getUser(rdb, username=target)
    if result == None:
        return {'msg': "No Signed Sign In Please....."}
    else:
        # cud ์„ธ์„ ์— result ์ถ”๊ฐ€
        rdb.expunge(result)  # ์„ธ์…˜ ๋ถ„๋ฆฌ
        cuddb.add(result)  # ์„ธ์…˜ ์ถ”๊ฐ€
        user = schemas.UserIn(
            username=username, password=password, nickname=nickname)
        result = crud.updateUser(cuddb, result, user)
        return result


@app.post('/delete/', response_model=Union[schemas.UserOut, Dict[str, str]], tags=['Delete'])
async def deleteUser(username: str, rdb: Session = Depends(get_rdb), cuddb: Session = Depends(get_cuddb)):
    result = crud.getUser(rdb, username=username)
    if result == None:
        return {'msg': "No User..."}
    else:
        # cud ์„ธ์„ ์— result ์ถ”๊ฐ€
        rdb.expunge(result)  # ์„ธ์…˜ ๋ถ„๋ฆฌ
        cuddb.add(result)  # ์„ธ์…˜ ์ถ”๊ฐ€
        result = crud.deleteUser(cuddb, result)
        return result

 

 

 

jsonable_encoder

FastAPI๋Š” JSON ํ˜ธํ™˜ ๊ฐ€๋Šฅ ๋ฐ์ดํ„ฐ๋งŒ ์ˆ˜์‹ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ํ†ต์‹ ์„ ์œ„ํ•ด jsonable_encoder() ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

ex. datetime ๊ฐ์ฒด๋Š” JSON๊ณผ ํ˜ธํ™˜๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋ถˆ๊ฐ€๋Šฅ, str๋กœ ๋ณ€ํ™˜๋˜์–ด์•ผ ํ•จ.

 

jsonable_encoder()์— ๊ฐ’์„ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

from fastapi.encoders import jsonable_encoder
from datetime import datetime

from pydantic import BaseModel


class Model(BaseModel):
    title: str
    timestamp: datetime
    desc: str


d = datetime.now()
m = Model(title='HI', timestamp=d, desc="test....")
print(f'{d}-> type: {type(d)}')
print(f'{m}-> type: {type(m)}')
print()
print(f'{jsonable_encoder(d)}-> type: {type(jsonable_encoder(d))}')
print(f'{jsonable_encoder(m)}-> type: {type(jsonable_encoder(m))}')

 

Model์€ dict์œผ๋กœ datetime์€ str๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

728x90
๋ฐ˜์‘ํ˜•

'Back-end & Server > FastAPI' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[FastAPI] Background Task  (0) 2024.02.23
[FastAPI] ํŒŒ์ผ ๋ถ„ํ•   (0) 2024.02.23
[FastAPI] ๋ฏธ๋“ค์›จ์–ด  (0) 2024.02.20
[FastAPI] ๊ฒฝ๋กœ ์ž‘๋™ ์„ค์ •  (0) 2024.02.20
[FastAPI] Form  (0) 2024.02.20