Django + Strawberryで作るGraphQLサーバ


この記事は[Django Advent Calendar 2022](https://qiita.com/advent-calendar/2022/django) 10日目の記事です。

# はじめに

今年は仕事が変わったのもあって、Djangoに触る機会が減りました。全くDjangoを触らなくなったのではなく、細々と自分のサイトで使っているのですが、Django + StrawberryでGraphQLサーバを作ると楽だぞという話をします。

# PythonのGraphQL事情

PythonのGraphQLと言えば[Graphene][]が有名です。[Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/)も触ってみて良い感触でしたが、開発が活発ではなくDjango 4.0の対応が遅れていたため(今は対応しています)、一時期は[Dead Project?](https://github.com/graphql-python/graphene-django/issues/1324)というIssueが立つほどでした。

そこで代わりに見つけたのが[Strawberry][]です。

# Strawberryの特徴

Python 3.7から採用されたデータクラス(dataclasses)に触発(インスパイア)されて作ったライブラリです。

> Strawberry is a new GraphQL library for Python 3, inspired by dataclasses.

こんな感じで定義できます。見やすいですね。

```py
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にも対応しています](https://strawberry.rocks/docs/integrations/django)。

以下のコードは自分がプライベートで作っているサイトのコードを元にしています。

https://zenn.dev/ikemo/articles/test-coverage-100-percent

## インストールするもの

- `strawberry-graphql[debug-server]`: Strawberryのコア(チュートリアルにあるもの)
- `strawberry-graphql-django`: Djangoとの統合機能

## モデルと対応したType

DjangoのモデルInboxに対応したInboxTypeは次のように書けます。フィールドは自前で定義する必要がありますが、勝手に定義されたりしないので安心です。

型はだいたい `auto` でいけます。Djangoのモデル定義を見て良きに計らってくれます。

```py
import strawberry_django
from strawberry import auto


@strawberry_django.type(Inbox)
class InboxType:
    id: auto
```

継承(Mixin)も可能ですが、こんな感じで `is_type_of` を定義しないといけない場合があります(GenericModelType、DjangoModelTypeは自分で作ったMixinです)。

```py
@strawberry_django.type(Inbox)
class InboxType(GenericModelType, DjangoModelType):
    @classmethod
    def is_type_of(cls, root, info):
        return isinstance(root, (cls, Inbox))
```

GraphQLだと逆参照を作りたくなる機会があるのですが、Django ORMでは逆参照がデフォルトで対応しているので簡単に書けます。

```py
@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はこんな感じです。簡単ですよね。

```py
@strawberry.enum
class InboxState(Enum):
    ACTIVE = "active"
    ALL = "all"
```

## Query

Queryはこんな感じです(型ヒントの書き方が古いですが)。Optionalにすると戻り値が必須から任意に変わります。

```py
@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は戻り値があるのに注意してください。

```py
@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
```

## マージ

こんな感じです。

```py
from strawberry.tools import merge_types

TaskQuery = merge_types(
    "TaskQuery",
    (
        ContextQuery,
        InboxQuery,
        LocationQuery,
        NextActionQuery,
        ProjectQuery,
        RepeatTaskQuery,
        SectionQuery,
        TaskInconsistenciesQuery,
    ),
)
```

## スキーマ

こんな感じで作成します。Query、Mutationはmerge_typesでマージしたもの、Typesはタイプのリストもしくはタプルを指定します。

```py
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個のタグが打たれています。ただ今のところ互換性でトラブったことはないです。

[Graphene]: https://docs.graphene-python.org/en/latest/
[Strawberry]: https://strawberry.rocks/