この記事はDjango Advent Calendar 2022 10日目の記事です。
はじめに
今年は仕事が変わったのもあって、Djangoに触る機会が減りました。全くDjangoを触らなくなったのではなく、細々と自分のサイトで使っているのですが、Django + StrawberryでGraphQLサーバを作ると楽だぞという話をします。
PythonのGraphQL事情
PythonのGraphQLと言えばGrapheneが有名です。Graphene-Djangoも触ってみて良い感触でしたが、開発が活発ではなくDjango 4.0の対応が遅れていたため(今は対応しています)、一時期はDead Project?というIssueが立つほどでした。
そこで代わりに見つけたのがStrawberryです。
Strawberryの特徴
Python 3.7から採用されたデータクラス(dataclasses)に触発(インスパイア)されて作ったライブラリです。
Strawberry is a new GraphQL library for Python 3, inspired by dataclasses.
こんな感じで定義できます。見やすいですね。
import typing
import strawberry
@strawberry.type
class Book:
title: str
author: str
@strawberry.type
class Query:
books: typing.List[Book]
他の多くのライブラリと同じくGraphiQL UIを内蔵していて、簡単に動作確認ができます。
Django ORMと組み合わせる
StrawberryはPythonのWebフレームワークとの統合機能があります。もちろんDjangoにも対応しています。
以下のコードは自分がプライベートで作っているサイトのコードを元にしています。
インストールするもの
-
strawberry-graphql[debug-server]
: Strawberryのコア(チュートリアルにあるもの) -
strawberry-graphql-django
: Djangoとの統合機能
モデルと対応したType
DjangoのモデルInboxに対応したInboxTypeは次のように書けます。フィールドは自前で定義する必要がありますが、勝手に定義されたりしないので安心です。
型はだいたい auto
でいけます。Djangoのモデル定義を見て良きに計らってくれます。
import strawberry_django
from strawberry import auto
@strawberry_django.type(Inbox)
class InboxType:
id: auto
継承(Mixin)も可能ですが、こんな感じで is_type_of
を定義しないといけない場合があります(GenericModelType、DjangoModelTypeは自分で作ったMixinです)。
@strawberry_django.type(Inbox)
class InboxType(GenericModelType, DjangoModelType):
@classmethod
def is_type_of(cls, root, info):
return isinstance(root, (cls, Inbox))
GraphQLだと逆参照を作りたくなる機会があるのですが、Django ORMでは逆参照がデフォルトで対応しているので簡単に書けます。
@strawberry_django.type(Book)
class BookType(DjangoModelType, GenericModelType):
@strawberry.field
def references(self: Book) -> List[ReferenceType]:
return self.reference_set.order_by("sort_order")
Enum
Enumはこんな感じです。簡単ですよね。
@strawberry.enum
class InboxState(Enum):
ACTIVE = "active"
ALL = "all"
Query
Queryはこんな感じです(型ヒントの書き方が古いですが)。Optionalにすると戻り値が必須から任意に変わります。
@strawberry.type
class InboxQuery:
@strawberry_django.field
def inboxes(self, state: InboxState = InboxState.ACTIVE) -> List[InboxType]:
if state == InboxState.ALL:
return Inbox.objects.all()
elif state == InboxState.ACTIVE:
return Inbox.objects.incomplete_only()
else: # pragma: no cover
raise AssertionError
@strawberry_django.field
def inbox(self, number: int) -> Optional[InboxType]:
return Inbox.objects.filter(pk=number).first()
Mutation
Mutationはこんな感じです。GraphQLのMutationは戻り値があるのに注意してください。
@strawberry.type
class InboxMutation:
@strawberry.mutation
def add_inbox(self, name: str) -> InboxType:
inbox = Inbox.objects.create(name=name)
return inbox
@strawberry.mutation
def done_inbox(self, inbox_id: int) -> InboxType:
inbox: Inbox = get_object_or_404(Inbox, pk=inbox_id)
inbox.status = InboxStatus.DONE
inbox.save()
return inbox
マージ
こんな感じです。
from strawberry.tools import merge_types
TaskQuery = merge_types(
"TaskQuery",
(
ContextQuery,
InboxQuery,
LocationQuery,
NextActionQuery,
ProjectQuery,
RepeatTaskQuery,
SectionQuery,
TaskInconsistenciesQuery,
),
)
スキーマ
こんな感じで作成します。Query、Mutationはmerge_typesでマージしたもの、Typesはタイプのリストもしくはタプルを指定します。
import strawberry
schema = strawberry.Schema(query=Query, mutation=Mutation, types=Types)
View
最後にViewです。GraphQLViewというクラスを使います。これは次のような継承関係になっています。
- strawberry.django.views.GraphQLView
- strawberry.django.views.BaseView
- django.views.generic.base.View
総評
GraphQLの概念に慣れるまで最初は大変でしたが、一旦セットするとメンテが楽です。特にTypeの書き方がシンプルでいいです。
他にもいろいろGraphQLライブラリ(主にTypeScript)を試してみましたが、だいたい2種類なんですよね。
- Resolverをスキーマから勝手に全部作ってくれるもの(Hasura, PostGraphileとか)
- Resolverを全部自分で書く必要があるもの
前者はセキュリティやフィールドをリネームしたときの互換性のことを考えると微妙だし、後者はMutationはともかくQueryは楽したいなぁと思っていました。
Strawberry + Djangoならフィールドは明示的に指定できますが、型は勝手に推測してくれるので楽です。RDBMSをそのままGraphQLの構造に変えるが、自由さは残したい、そんなときにちょうどいいです。
注意が必要なところとしては、まだ1.0未満で、アップデートがめちゃくちゃ多いです。2022/12/5現在で488個のタグが打たれています。ただ今のところ互換性でトラブったことはないです。