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

댓글

이 블로그의 인기 게시물

북궐도 2.0

Arch 계열 리눅스 구글 크롬 설치