FastAPI, CRUD API 개발을 위한 기록
API에 있어 기본이 되는 CRUD API 를 개발하며 FastAPI 에 적응하기 위한 기록을 남긴다.
1. 환경세팅
1) 프로젝트 생성
poetry new apps
poetry 를 통해 프로젝트를 생성하면 아래와 같은 구조를 갖게 된다.
apps
├── pyproject.toml
├── README.rst
├── apps
│ └── __init__.py
└── tests
├── __init__.py
└── test_apps.py
2) 의존성 라이브러리 설치
db는 mysql 을 사용할 것이고, db 비동기 커넥션은 encode.io 에서 개발한 databases 를 사용해보기로 했다. poetry 로 아래와 같이 필요한 라이브러리를 설치 한다.
poetry add 'uvicorn[standard]' fastapi sqlalchemy 'databases[mysql]' python-dotenv mysqlclient
- uvicorn[standard]
ASGI Web Server - fastapi
ASGI Framework - databases[mysql]
db 비동기지원 드라이버 - sqlalchemy
ORM toolkit - python-dotenv
환경변수 관리
3) DB 도커 컨테이너 세팅
docker run -it --name db -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mariadb
2. DB 접속 환경 개발
fastapi 환경에서 공통적으로 사용할 db 접속환경에 관련된 부분을 개발 해본다. 파일의 위치와 용도는 다음과 같다.
apps
├── .env # 환경변수 정의
├── pyproject.toml
├── README.rst
├── apps
│ └── __init__.py
│ └── database.py # 디비접속 환경정보 기술
│ └── main.py # application main
└── tests
├── __init__.py
└── test_apps.py
1) .env
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_USER=root
DB_PASSWD=root
DB_PORT=3306
DB_NAME=test
.env 파일로 선언한 것을 load_dotenv 함수로 로드해서 os.environ 환경변수로 받아올 것이다.
2) database.py
프로젝트 전체에서 사용할 db 접속환경을 세팅 해주는 부분이다.
# database.py
from databases import Database
from os import environ
from dotenv.main import load_dotenv
from sqlalchemy import create_engine, MetaData
# .env 환경파일 로드
load_dotenv()
# 디비 접속 URL
DB_CONN_URL = '{}://{}:{}@{}:{}/{}'.format(
environ['DB_TYPE'],
environ['DB_USER'],
environ['DB_PASSWD'],
environ['DB_HOST'],
environ['DB_PORT'],
environ['DB_NAME'],
)
# 쿼리를 위한 db 커넥션(비동기) 부분
db_instance = Database(
DB_CONN_URL,
min_size=5, # 기본 connection 수
max_size=1000, # 최대 connection 수
pool_recycle=500 # mysql 에 설정된 wait_timeout 시간 동안 재요청이 없을 경우 MySQL에서 해당 세션의 연결을 끊어버린다 (https://yongho1037.tistory.com/569)
)
# 모델 초기화를 위한 db 커넥션 부분
db_engine = create_engine(DB_CONN_URL)
db_metadata = MetaData()
3) main.py
# main.py
from fastapi import FastAPI, Request
from apps.database import db_instance
app = FastAPI(
title="Memo API",
description="Memo CRUD API project",
version="0.0.1"
)
# 서버 시작시 db connect
@app.on_event("startup")
async def startup():
await db_instance.connect()
# 서버 종료시 db disconnect
@app.on_event("shutdown")
async def shutdown():
await db_instance.disconnect()
# fastapi middleware, request state 에 db connection 심기
@app.middleware("http")
async def state_insert(request: Request, call_next):
request.state.db_conn = db_instance
response = await call_next(request)
return response
3. Memo, CRUD 기능 개발하기
Memo 기능을 구현하기 위해 아래와 같이 프로젝트 구조를 잡아줬다
apps
├── .env # 환경변수 정의
├── pyproject.toml
├── README.rst
├── apps
│ └── __init__.py
│ └── database.py # 디비접속 환경정보 기술
│ └── main.py # application main
│ └── memo
│ └── model.py
│ └── schema.py
│ └── router.py
└── tests
├── __init__.py
└── test_apps.py
1) Model
memo 데이터를 저장할 db 테이블을 정의해보자
# model.py
from datetime import datetime
from apps.database import db_engine, db_metadata
from sqlalchemy import Table, Column, String, Integer, DateTime
memo = Table(
"memo",
db_metadata,
Column("idx", Integer, primary_key=True, autoincrement=True),
Column("regdate", DateTime(timezone=True), nullable=False, default=datetime.now),
Column("title", String(255), nullable=False),
Column("body", String(2048), nullable=False)
)
# 테이블 정보로 테이블 생성한다
memo.metadata.create_all(db_engine)
2) 기본 Router
주요 CRUD 기능을 구현하기 전에 간단한 기능을 router 에 구현하고 main 에 적용하도록 하자
# router.py
from fastapi.routing import APIRouter
memo_router = APIRouter()
@memo_router.get("/test")
async def test():
return {"say":"hello"}
위에서 간단히 구현한 router를 아래와 같이 main 에 붙이고 기능확인을 한다
# main.py
from fastapi import FastAPI, Request
from apps.database import db_instance
from apps.memo.router import memo_router
app = FastAPI(
title="Memo API",
description="Memo CRUD API project",
version="0.0.1"
)
@app.on_event("startup")
async def startup():
await db_instance.connect()
@app.on_event("shutdown")
async def shutdown():
await db_instance.disconnect()
@app.middleware("http")
async def state_insert(request: Request, call_next):
request.state.db_conn = db_instance
response = await call_next(request)
return response
# 이 부분을 추가하여 router 구현로직을 적용
app.include_router(memo_router)
3) Create 기능 구현하기
3-1) Create, Schema
Memo, Create 기능 구현을 위해, 아래와 같이 BaseModel 를 상속받는 MemoCreate 클래스를 생성해줬다.
# schema.py
from datetime import datetime
from pydantic import BaseModel
# Pydantic을 이용한 Type Hinting
class MemoCreate(BaseModel):
regdate : datetime
title : str
body : str
3-2) Create, Router
# router.py
from databases.core import Database
from fastapi.param_functions import Depends
from fastapi.routing import APIRouter
from fastapi.requests import Request
# Memo 스키마
from apps.memo.schema import MemoCreate
# Memo 모델
from apps.memo.model import memo
memo_router = APIRouter()
# 디비 쿼리를 위한 종속성 주입을 위한 함수
def get_db_conn(request: Request):
return request.state.db_conn # middleware 에서 삽입해준 db_conn
# 메모 생성
@memo_router.post("/memo")
async def memo_create(
req: MemoCreate, # 요청을 정의한 create 스키마에 맞게 전달 받음
db: Database = Depends(get_db_conn)
):
query = memo.insert() # insert 쿼리 생성 (feat.sqlalchemy)
# validation 처리가 필요할 듯
values = req
await db.execute(query, values) # 쿼리 실행 (feat.databases)
return {**req.dict()}
3-3) 구현기능 확인
4) Select 기능 구현하기
4-1) Select, Schema
# schema.py
from datetime import datetime
from pydantic import BaseModel
# Pydantic을 이용한 Type Hinting
class MemoCreate(BaseModel):
regdate : datetime
title : str
body : str
class MemoSelect(BaseModel):
idx : int
regdate : datetime
title : str
body : str
4-2) Select, Router
@memo_router.get("/memo/find/{idx}", response_model=MemoSelect)
async def memo_findone(
idx: int,
db: Database = Depends(get_db_conn)
):
query = memo.select().where(memo.columns.idx == idx)
return await db.fetch_one(query)
@memo_router.get("/memo/list/{page}", response_model=List[MemoSelect])
async def memo_findone(
page: int = 1,
limit: int = 10,
db: Database = Depends(get_db_conn)
):
offset = (page-1)*limit
query = memo.select().offset(offset).limit(limit)
return await db.fetch_all(query)
4-3) 구현기능 확인
5) Delete 기능 구현하기
5-1) Delete, Router
@memo_router.delete("/memo/{idx}")
async def memo_findone(
idx: int,
db: Database = Depends(get_db_conn)
):
query = memo.delete().where(memo.columns.idx == idx)
await db.execute(query)
return {"result": "success"}