Python Strawberry GraphQL 예제 (feat. #sqlmodel, #mysql)
API보다 유연한 무엇을 찾아 나서다
API를 기획하거나 제작 하다보면 엔드포인트에서 유연하게 작동할 수 있는 무엇(?)을 원하게 된다. 그러다보면 API에서 필드의 스콥을 정하는 필드를 만드는 악수를 두는 상황이 벌어질 수도 있다 중급이상 개발자야 알고있거나 찾아놨거나 만들었겠지만 뭐 여튼 이래저래 찾다보면 GraphQL을 만나게 된다.
GraphQL
GraphQL은 애플리케이션 프로그래밍 인터페이스(API)를 위한 쿼리 언어이자 서버측 런타임으로 클라이언트에게 요청한 만큼의 데이터를 제공하는 데 우선 순위를 부여한다 바로 이점 에서 장점이 생기는데 이부분으로 인해 차후 네트워크 대역폭을 절약시키는 데 도움을 주기도 한다.
파이썬 (#python)
최근들어서 #머신러닝 이나 #AI 관련 프로젝트에 대한 이야기만 나오면 나오는 언어가 바로 파이썬이다. GraphQL공식 사이트 가보면 알겠지만 프로젝트의 큰 부분을 차지하는게 JS 지만 관리 하는 입장이라면 아무래도 통합적으로 관리하기를 원할 수 있다보니 API 도 파이썬으로 개발 운영 해보는걸로 택했다.
FastAPI + GraphQL (#strawberry) + SQLmodel
파이썬에 대해서 알아가는 중이기도 하지만 조금만 찾아보면 API만든다고 하면 많은 메뉴얼이 뜨고 있는 FastAPI(이름부터 마음에 듬) 를 택했다.
문제는 GraphQL 작성을 위한 프레임워크 인데 가장 많이 쓰이고 있는게 Graphene (star 7.7k) 이다. 코드를 보니 크게 어려운건 없지만 2위의 #strawberry 가 유독 끌렸다. 깃헙에서 스타갯수도 3.2k 로 한참 밀리는 느낌이지만 릴리즈 수가 압도적이었다. (어떤면에서 불안할 수 있다고 할 수 있겠지만 크게 Deprecated 되는 것이 없었다) ORM은 직관적이라고 판단한 SQLmodel을 활용하기로 했다.
패키지 관리
패키지 관리툴은 poetry를 사용 했다.
[tool.poetry.dependencies]
python = "^3.9"
mysqlclient = "^2.1.1"
python-dotenv = "^0.21.0"
databases = {extras = ["mysql"], version = "^0.6.1"}
fastapi = "^0.95.0"
SQLAlchemy = "^1.4.41"
uvicorn = {extras = ["standard"], version = "^0.18.3"}
strawberry-graphql = {extras = ["fastapi"], version = "^0.177.0"}
sqlmodel = "^0.0.8"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
python-mecab-ko = "^1.3.3"
models.py
from typing import Optional, List, Union, Dict
from pydantic import condecimal, BaseModel
from os import environ
from dotenv.main import load_dotenv
from sqlmodel import (
SQLModel,
Field,
create_engine,
Relationship
)
# .env 환경파일 로드
load_dotenv()
# DB connection (모양을 이쁘게?)
DB_CONN_URL = '{}://{}:{}@{}:{}/{}'.format(
environ['DB_TYPE'],
environ['DB_USER'],
environ['DB_PASSWD'],
environ['DB_HOST'],
environ['DB_PORT'],
environ['DB_NAME'],
)
engine = create_engine(DB_CONN_URL, echo=True)
class TeamModel(SQLModel, table=True):
__tablename__ = "team"
id: Optional[int] = Field(default=None, primary_key=True)
name: str
headquarters: str
heroes: List["HeroModel"] = Relationship(back_populates="team")
class HeroModel(SQLModel, table=True):
__tablename__ = "hero"
id: Optional[int] = Field(default=None, primary_key=True)
name: str
secret_name: str
age: Optional[int] = None
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[TeamModel] = Relationship(back_populates="heroes")
# 테이블이 생성 (유연하게 하고 싶으면 주석 처리)
SQLModel.metadata.create_all(engine)
resolver.py
from typing import Optional, List, Union, Dict
from sqlmodel import Session, select
from sqlalchemy.orm import joinedload, lazyload
from datetime import datetime, timedelta
from models import (
TeamModel
,HeroModel
,engine
)
def get_heroes():
with Session(engine) as session:
heroes = session.exec(select(HeroModel)).all()
return heroes
def get_teams():
with Session(engine) as session:
# use eager loading for up to three level nesting. probably want to add this dynamically.
heroes = session.exec(select(TeamModel).options(selectinload(TeamModel.heroes).selectinload(HeroModel.team))).all()
return heroes
graphql.py
from typing import Optional, List, Dict, Union, Annotated, Tuple
from pydantic import condecimal, BaseModel
import decimal
import strawberry
from strawberry.fastapi import GraphQLRouter
from resolver import (
get_heroes
,get_teams
)
@strawberry.type
class Hero:
id: Optional[int]
name: str
secret_name: str
age: Optional[int]
team_id: Optional[int]
team: Optional[strawberry.LazyType["Team", __module__]]
@strawberry.type
class Team:
id: Optional[int] = Field(default=None, primary_key=True)
name: str
headquarters: str
heroes: List["Hero"]
@strawberry.type
class Query:
allHeroes: List[Hero] = strawberry.field(resolver=get_heroes)
allTeams: Team = strawberry.field(resolver=get_teams)
schema = strawberry.Schema(
query=Query,
# mutation=Mutation
)
graphql_app = GraphQLRouter(schema)
main.py
from typing import Optional, List, Union, Annotated
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from graphql import graphql_app
app = FastAPI()
app.add_middleware(
CORSMiddleware,
# allow_origins=["*"],
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(graphql_app, prefix='/graphql')
@app.get("/test")
async def test_obj():
return {"test" : "test message" }
실행
$ poetry run uvicorn main:app --reload --host 0.0.0.0 --port 8080
댓글
댓글 쓰기