[FastAPI] 13. SQLAlchemy์ Pydantic์ ์ด์ฉํ ๊ด๊ณ ๋ฐ์ดํฐ ๋งคํ
SQLAlchemy๋ฅผ ์ฌ์ฉํ๋ค๋ณด๋ฉด ์ํ์ง ์์ ๋ API์์ ๋ชจ๋ ์ปฌ๋ผ์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ต์ ํ ํ๊ธฐ๊ฐ ์ด๋ ค์ธ ๋๊ฐ ์์ต๋๋ค. ์ด๋ค API์์๋ ํน์ ์ปฌ๋ผ์ ๋ํ ๋ฐ์ดํฐ ํน์ ๊ด๊ณ ๋ฐ์ดํฐ๊ฐ ํ์ํ ๋๊ฐ ์๋๋ฐ, ๊ทธ๋ ์ง ์์ ๋ฐ์ดํฐ๊น์ง ๋ชจ๋ ๋์ค๊ฒ ๋์ด ์คํ๋ ค API ๋ก๋ฉ ์๋๋ฅผ ์ ํ์ํค๊ณ ์๋ฒ ๋ถํ์ ์์ธ์ด ๋๊ธฐ๋ ํฉ๋๋ค.
SQLAlchemy ORM์ relationship
SQLAlchemy ORM์์๋ ๊ด๊ณ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด relationship์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด ๋ธ๋ก๊ทธ๋ฅผ ํ๋ ๋ง๋๋ ค๋๋ฐ, ์ด๋ค ์นดํ ๊ณ ๋ฆฌ์ ๊ธ์ธ์ง๋ฅผ ์๊ธฐ ์ํด์ ์๋์ ๊ฐ์ด ๋ฐ์ดํฐ๋ฅผ ์ค๊ณํด ๋ณผ ์ ์์ต๋๋ค.

์นดํ ๊ณ ๋ฆฌ๋ ํ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ๊ฐ์ง ์ ์๊ณ , ํ๋์ ์ปจํ ์ธ ๋ ์นดํ ๊ณ ๋ฆฌ ํ๋๋ฅผ ๊ฐ์ง ์ ์๋ค๋ผ๊ณ ํ์ ๋ ์์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ค๊ณํด ๋ณผ ์ ์์ต๋๋ค. (์ ์ค๊ณ๋ ๋ฐ๋์ ์ ๋ต์ด ๋ ์๋ ์์ต๋๋ค.)
์ ์ํฉ์์ SQLAlchemy ORM์ผ๋ก ์ฝ๋๋ฅผ ์ง ๋ค๋ฉด ์๋์ ๊ฐ์ ์ฝ๋ ํํ๊ฐ ๋ฉ๋๋ค.
from sqlalchemy.ext.declarative import declarative_base | |
Base = declarative_base() | |
class CategoryEntity(Base): | |
id: Union[int, Column] = Column(BigInteger, primary_key=True, autoincrement=True) | |
name: Union[str, Column] = Column(String(50), nullable=False) | |
parent_id: Union[int, Column] = Column(ForeignKey('category.id', ondelete='RESTRICT'), nullable=True) | |
class PostEntity(Base): | |
__tablename__ = 'post' | |
id: Union[int, Column] = Column(BigInteger, primary_key=True, autoincrement=True) | |
title: Union[str, Column] = Column(String(120), nullable=False, index=True) | |
body: Union[str, Column] = Column(Text, nullable=True) | |
thumbnail: Union[AnyHttpUrl, Column] = Column(String(255), nullable=True) | |
is_private: Union[bool, Column] = Column(Boolean, nullable=False, default=True) | |
published_at: Union[datetime, Column] = Column(DateTime(), nullable=True, index=True) | |
category = relationship('CategoryEntity' foreign_keys=category_id) | |
category_id: Union[int, Column] = Column(ForeignKey('category.id', ondelete='RESTRICT'), nullable=True) | |
description: Union[str, Column] = Column(String(1024), nullable=False, default='No description') |
๋ธ๋ก๊ทธ ์ปจํ ์ธ ๋ฅผ ์กฐํํ์ ๋ ์ด๋ค ์นดํ ๊ณ ๋ฆฌ์ธ์ง๋ฅผ ํ์ธํ๊ธฐ ์ํด relationship์ ์ฌ์ฉํ์ฌ ์ฐ๊ฒฐ๋ ์นดํ ๊ณ ๋ฆฌ๋ฅผ CategoryEntity๋ก ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
Pydantic with relationship
FastAPI์์ response๋ก ์๋ตํ๊ธฐ ์ํด์๋ ์ด๋ป๊ฒ ํด์ผํ ๊น์? ๊ฐ์ฅ ๊ฐ๋จํ๊ฒ๋ ORM ๋ชจ๋ธ์ JsonDecoder ๋ฑ์ ์ด์ฉํ์ฌ JSON์ผ๋ก ๋ณํํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ DateTime์ด๋ ์ง๊ธ์ฒ๋ผ relationship์ ์ด์ฉํ๋ ๊ฒฝ์ฐ ํด๋น ๋ชจ๋ธ์์ ์ฌ์ฉํ๋ ๋ชจ๋ ํ์ ์ ๋ํด JSON ํ์ ์ผ๋ก ๋ณํํด์ผ ํ๋ ๋ก์ง์ ์ผ์ผ์ด ์ํํด์ค์ผ ํ๋ค๋ ๋จ์ ์ด ์กด์ฌํฉ๋๋ค.
์ด๋ฅผ ์ข ๋ ํธํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด Pydantic์ ๋ด์ฅ๋ ORM Mode์ FastAPI์ jsonable_encoder๋ฅผ ์ฌ์ฉํ๋ฉด ์ฐ๋ฆฌ๊ฐ ์ง์ JSON ํ์๋ฅผ ๊ตฌํํ์ง ์์๋ Pydantic ๋ชจ๋ธ๋ก ๋ณํํด์ฃผ๊ณ ์ด์ ๋ง์ถฐ JSON ํฌ๋งท์ผ๋ก ์ธ์ฝ๋ฉ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ๋ ๊ฐ์ง ํธ๋ฆฌํ ์ด์ ์ ์ป์ด๋ผ ์ ์์ต๋๋ค.
from pydantic import BaseModel | |
class CategoryInfo(BaseModel): | |
id: int = Field(None, title="์นดํ ๊ณ ๋ฆฌ ๊ณ ์ ๋ฒํธ") | |
name: str = Field(None, title="์นดํ ๊ณ ๋ฆฌ ์ด๋ฆ") | |
class Config: | |
orm_mode = True | |
class PostModel(BaseModel): | |
id: int = Field(0, title="์ปจํ ์ธ ๋ฒํธ") | |
title: str = Field(title="์ปจํ ์ธ ์ ๋ชฉ", max_length=120) | |
body: Optional[str] = Field(title="์ปจํ ์ธ ๋ด์ฉ") | |
description: Optional[str] = Field(title="์ปจํ ์ธ ์์ฝ ๋ด์ฉ", max_length=1024) | |
category: Optional[CategoryInfo] = Field(title="์ปจํ ์ธ ์นดํ ๊ณ ๋ฆฌ ์ ๋ณด") | |
thumbnail: Optional[HttpUrl] = Field(None, title="์ปจํ ์ธ ์ฌ๋ค์ผ ์ด๋ฏธ์ง") | |
is_private: bool = Field(True, title="์ปจํ ์ธ ๊ณต๊ฐ ์ฌ๋ถ") | |
created_at: Optional[datetime] = Field(None, title="์ปจํ ์ธ ์์ฑ ๋ ์ง ๋ฐ ์๊ฐ") | |
updated_at: Optional[datetime] = Field(None, title="์ปจํ ์ธ ์ต์ข ์์ ๋ ์ง ๋ฐ ์๊ฐ") | |
published_at: Optional[datetime] = Field(None, title="์ปจํ ์ธ ์ต์ด ๋ฐํ ๋ ์ง ๋ฐ ์๊ฐ") | |
class Config: | |
orm_mode = True |
Pydantic์์ BaseModel์ Python์ dict ํํ์ ๋ฐ์ดํฐ๋ฅผ Pydantic ๋ชจ๋ธ๋ก ๋ณํํ๋ ๋ฐ ์ฌ์ฉํ๋ฉฐ ์ฌ๊ธฐ์ orm_mode๋ฅผ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ SQLAlchemy์ ORM Model ํํ์ ๋ฐ์ดํฐ๋ฅผ Pydantic ๋ชจ๋ธ๋ก ๋ณํํ๋ ๋ก์ง์ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
์ด๋ฅผ API๋ก ๊ตฌํํ๋ฉด ์๋์ ๊ฐ์ด ๊ตฌํํ ์ ์์ต๋๋ค.
from fastapi import FastAPI | |
# import PostModel | |
app = FastAPI() | |
@app.get('', response_model=PostModel) | |
async def get_posts(): | |
# return Post ORM Model |
์ฌ๊ธฐ์ ๊ตฌ์ฒด์ ์ธ ์ฝ๋๋ ์ ์ง ์๋๋ก ํ๊ฒ ์ต๋๋ค. ์ฌ๋ฌ๋ถ๋ค์ด ์ด๋ค ํํ๋ก ์ฌ์ฉํ๋์ง์ ๋ฐ๋ผ ์ฌ๋ถ๊ฐ ๋ฌ๋ผ์ง ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. ์ค์ํ ์ ์ response_model์ ์์์ ์ ์ํ Pydantic ๋ชจ๋ธ์ ์ ์ํ๋ฉด ๋๊ณ api์์ ๋ฐํํ๋ ๋ฐ์ดํฐ๋ ORM ๋ชจ๋ธ๋ง ๋ฐํํ๋ฉด ๋ฉ๋๋ค.
๊ทธ๋ฐ๋ฐ, ์ด๋ป๊ฒ API์์ ORM ๋ชจ๋ธ๋ง ๋ฐํํ๋๋ฐ, ๋ฐ๋ก Pydantic ๋ชจ๋ธ์ด ๋์ด JSON์ผ๋ก ๋์ฌ ์๊ฐ ์๋ ๊ฒ์ผ๊น์?
FastAPI jsonable_encoder
FastAPI์์๋ ์๋ฒ ๋ฐ์ดํฐ ๋ฐํ์ JSON์ผ๋ก ๋ณํํ๊ธฐ ์ํ ์ธ์ฝ๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด ์ธ์ฝ๋๋ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋ json์ dumps์ ๊ฐ์ ๋ก์ง์ด ์๋๋๋ค. ์ฐ๋ฆฌ๊ฐ ์ํ๋ค๋ฉด ujson์ ์ธ ์๋ ์๊ณ orjson์ ์ธ ์๋ ์์ผ๋ฉฐ ์ด์ ๋ง์ถฐ์ ์๋์ผ๋ก ๋ฐ์ดํฐ ๊ฐ์ ์ธ์ฝ๋ฉํด์ฃผ๋ ์์ฃผ ์ข์ ๋ชจ๋์ ๋๋ค.
์ค์ ๋ก FastAPI๋ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ก Pydantic์ ์ฑํํ๊ณ ์์ผ๋ฉฐ response_model์ Pydantic ๋ชจ๋ธ์ ๋ช ์ํ๋ ๊ฒฝ์ฐ jsonable_encoder ํจ์๊ฐ ์๋ํฉ๋๋ค. ์ด ๋ jsonable_encoder๋ ์ค์ง dict ํํ์ ๋ฐ์ดํฐ๋ง์ json ๊ฐ์ผ๋ก ๋ฐํํด์ฃผ๋๋ฐ, orm_mode๊ฐ ์ค์ ๋์ด ์๋ ๊ฒฝ์ฐ Pydantic์ from_orm์ด๋ผ๋ ํจ์์ ์๋ ๋ก์ง ๊ทธ๋๋ก๋ฅผ ๋ฐํํ์ฌ json ๊ฐ์ผ๋ก ๋ณํํด์ฃผ๊ฒ ๋ฉ๋๋ค.

์ ์ฌ์ง์ ์ค์ ๋ก FastAPI๊ฐ response_model์ ์ ์ํ์์ ๋ ๋์ํ๋ ์ฝ๋์ ๋๋ค. Pydantic ๋ชจ๋ธ์ ์ ์ํ์์ ๋๋ ๋ฐ๋ก dict๋ก ๋ณํํด์ json์ผ๋ก ๋ฐํํ๊ณ , orm_mode๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์๋ from_orm ํจ์์ ์์กดํ ์ฑ ๋ฐ๋ก json์ผ๋ก ๋ณํํ๊ฒ ๋ฉ๋๋ค.
์ฌ๊ธฐ์ from_orm์ FastAPI์์ ์ ๊ณตํ๋ ํจ์๊ฐ ์๋ Pydantic BaseModel์์ ์ ๊ณตํ๋ classmethod๋ก์จ FastAPI๋ dict๋ก ๋ณํ ์์ด ์ด ๋ฐ์ดํฐ๋ง์ ์ด์ฉํด json์ผ๋ก ๋ฐํํฉ๋๋ค.

์ฝ๋๋ฅผ ๋ณด๋ฉด SQLALchemy ๋ชจ๋ธ์์ ์๋์ผ๋ก dict๋ก ๋ณํํ ๋ ์ฌ์ฉํ๋ ํ์ด์ฌ ๊ฐ์ฒด ๋ฉ์๋์ __dict_-์ __fieds_set__์ ๋ณด๊ณ ๊ฐ์ validation ํ์ฌ ๊ฐ์ ธ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
๋ง์น๋ฉฐ...
relationship์ ์ด์ฉํ์ฌ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฅผ ์ด๋ค์์ผ๋ก json ๊ฐ์ ๋ฃ์ด ๋ณด๋ด๋์ง๋ฅผ ์์๋ดค์ต๋๋ค. ๊ฐ๋จํ ์ฐ๊ธฐ์๋ ์ด๋ ค์ ๋ณด์ด์ง ์์ง๋ง ManyToOne, ManyToMany ๋ฑ ๋ค์ํ ๊ด๊ณ์์ ์ฌ์ฉํ๊ธฐ์๋ ์์ง ๋ค์ ๋ถ์กฑํ ๋ถ๋ถ์ด ์์ต๋๋ค. ์ด ๋ถ๋ถ์ ๋ํด์๋ ๋ค์ ํฌ์คํธ์์ ๋ค๋ค๋ณผ ๊ฒ์ ๋๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ FastAPI์์ ์ด๋ค ์์ผ๋ก relationship์ ๋ค๋ฃจ๊ณ ์ฌ์ฉํ๋ ๊ฒ์ ๋ํด ๊ฐ๋จํ ๋ค๋ฃจ๋ ๊ฒ์ ๋ชฉ์ ์ผ๋ก ํ์์ผ๋ฉฐ ์ฐ๋ฆฌ๋ ์ด ๋ฐฉ์์ด ์ด๋ค ๋ฐฉ์์ธ์ง๋ฅผ ์ด์ ์์์ผ๋ ๋ค์ํ ๋ฐฉ๋ฒ์ relationship ์ฌ์ฉ๋ฒ์ ์์๋ณด๋ฉด์ ์ฟผ๋ฆฌ๋ฅผ ์ต์ ํ ํ๋ ๋ฐฉ๋ฒ์ ๋ณด๋ฉด ๋๊ฒ ์ต๋๋ค.