delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Ariadne ✖️ DjangoでスキーマファーストのGraphQL開発を体験してみた

はじめに

この記事はDjango Advent Calendar 2023の17日目の記事です。

本記事ではAriadneというスキーマファーストの思想のPythonのGraphQLライブラリとDjangoを使って、スキーマファーストのGraphQL開発に入門してみた経過と感想を書きます。 ariadnegraphql.org

バージョン

本記事で使用したバージョンは以下です。

動機

GraphQLのライブラリは以下の2つの思想に分類されます。

それぞれのメリット・デメリットについてはたくさん議論されているので、ここでは割愛します。 www.apollographql.com

私は仕事では、Strawberryというコードファーストの思想のGraphQLライブラリを使っています。 strawberry.rocks

Strawberryに関してはWEB+DB PRESS Vol.135で記事を書かせて頂き、また仕事で同じチームの@mysh_iiiiさんはDjango Congress2023で発表してくださりました。

www.docswell.com

Starwberryに特に不満はないのですが、スキーマファーストの思想のライブラリも触ってみたいなと思っていました。

そこで、Pythonスキーマファースト系のGraphQLライブラリで唯一GraphQLの公式サイトで紹介されており、GitHubのスター数も2000を超えているAriadneを技術検証がてらに触ってみることにしました。

※ 厳密にはTartifletteというライブラリも紹介されていますが、こちらは開発が停滞しているようです。

AriadneのDjangoのサポート状況

まずは、AriadneがDjangoをサポートしているのかが気になります。 結論からいうと、以下のような状態のようです。

Ariadneのリポジトリ本体はDjangoをサポートしていない

公式のTOPには以下のように紹介されており、Djangoをサポートしているように見えます。

しかし、ariadne.contrib.djangoというパッケージは以下の2021年のPRで削除されています。 github.com

公式のDjangoのIntegrationのページをみると、ariadne-djangoを使ってねとのことのようです。

ariadne-djangoはメンテされていない様子である

そのariadne-djangoですが、以下の理由よりほぼメンテされていない様子であることが伺えます。

  • 最終コミットが1年以上前
  • tox.iniをみるとDjango4系に対応していない

本記事における方針

本記事においては、以下の理由よりariadne-djangoを使うことにします。

  • Django 4.2でも問題なく動いた
  • あくまでスキーマファースト系のGraphQLを体験してみることが目的

実務で使う場合

実務の場合はさすがにメンテされていないものをブラックボックスのまま使うわけにはいかないので、以下のような方針の検討が必要になりそうです。

  • ariadne-djangoのコード量自体は多くないので、読んで理解した上で使う
  • Djangoとのインテグレーション部分は自前で実装する。
  • Ariadne本体はメンテされているので、Djangoとの連携は諦める(=Djangoを使わない)

触ってみた経過

ここからは触ってみた経過を書きます。

Djnagoのプロジェクトの作成

まずは普通に公式のチュートリアル通りにDjnagoのプロジェクトを作成します。

※ 公式ではpollsというapplicationを作成していますが、本記事では本を題材にするため、booksとしています。

Ariadneのインストール

ariadneariadne-djangoをインストールします。

pip install ariadne ariadne-django

INSTALLED_APPSへの追加

ariadne-djangoのREADMEに記載の通り、INSTALLED_APPSariadne_djangoを追加します。

INSTALLED_APPS = [
    # ...
    "ariadne_django",
    "books",
]

スキーマファイルの作成

ここがスキーマファーストのGraphQLを使う場合の特徴的な工程です。

DjangoのPJディレクトリ直下(=manage.pyと同じ階層)にschema.graphqlというファイルを作成します。

schema.graphqlに以下を記述していきます。

  • GraphQLの型
  • Query
  • Mutation
type Book {
    id: ID
    title: String!
    price: Int 
}

type User {
    id: ID
    username: String!
    books: [Book]
}

type Query {
    me: User
}

input RegisterBookInput {
    title: String!
    price: Int
}

type Mutation {
    registerBook(input: RegisterBookInput!): Book!
}

GraphQLのスキーマの記法の説明はここでは詳しくしませんが、型の後ろにビックリマークをつけているのはNot Nullという意味です。

補足説明

schema.graphqlはコードファーストのライブラリではコードをインプットとして自動生成される成果物ですが、スキーマファーストのライブラリでは手書きでコード実装よりも先に書きます。

一見コードファーストのライブラリと比べて作業が増えるデメリットしかないように感じます。

しかし、フロントエンドが別のチームや会社だったりする場合には、スキーマファイルをGraphQLのインターフェース策定のコミュニケーションツールとして使えるため、トータルでスキーマファーストの方が効率がいい場合も多いそうです。(この辺の話題は「スキーマ駆動開発」でググると情報がたくさん見つかります)

また今回は少量なので、補完ツールなどなしで手書きで書きましたが、業務のPJなどで量が多い場合はスキーマファイルを編集するための環境を整備した方がよいそうです。 zenn.dev

スキーマファイルを読み込む

作成したスキーマファイルは以下のように読み込むことができます。読み込んだスキーマの使い方は後述します。

from ariadne import load_schema_from_path

schema = load_schema_from_path("schema.graphql")

上記はmysite/mysite/schema.pyというファイルを作成してそこに記述する方針とします。以降、特に説明がなければコードは全てschema.pyに書く前提とします。

meクエリーの実装

まずは「meクエリー」というログインユーザーの情報を返すクエリーを実装していきます。

QueryTypeインスタンスを作成する

以下のようにQueryTypeというクラスのインスタンスを作成します。

from ariadne import QueryType

QueryType = QueryType()

ゾルバを定義する

ariadnegraphql.org

スキーマmeのクエリは定義済なので、以下のようにリゾルバを実装して紐づけます。

from graphql import GraphQLResolveInfo

# ...

@QueryType.field("me")
def resolve_me(
    _, info: GraphQLResolveInfo
) -> Optional[User]:
    user = info.context["request"].user
    if user.is_anonymous:
        return None
    return User.objects.get(pk=user.pk)

Djangoのリクエストオブジェクトはinfo.context["request"]から取得できるので、一般のDjangoアプリケーションと同じように認証できます。

なお、DjangoのModelからGraphQL型への変換はAriadneがやってくれました。 この辺のマッピングは自前で実装しなければいけないと思っていたので嬉しい誤算でした。

スキーマQueryTypeを紐づける

ariadnegraphql.org

スキーマQueryTypeを紐づけます。

以下のように読み込んだスキーマファイルとQueryTypemake_executable_schemaという関数に渡します。

schema = make_executable_schema(load_schema_from_path("schema.graphql"), [QueryType,],)

mysite/urls.pyにエンドポイントを定義する

github.com

以下のようにmysite/urls.pyにGraphQLのエンドポイントを定義します。

from ariadne_django.views import GraphQLView
from django.urls import path

from .schema import schema

urlpatterns = [
    path(
        "graphql/",
        GraphQLView.as_view(schema=schema),
        name="graphql",
    ),
]

ここまでで、ブラウザから「meクエリ」を実行できるようになります。

registerBookの実装

次にregisterBookというログインユーザーが本を登録するMutationを実装します。

Input型を定義する

まずはMutationのリクエストパラメータとなるInput型を定義します。 いくつか書き方があるようですが、公式ドキュメントの以下のセクションに記載されているように、いったんdataclassで定義してラムダ関数で渡すのがタイプヒントが効いてかつ簡潔に書けるので保守性が高そうです。

ariadnegraphql.org

from ariadne import InputType

# ...

@dataclass
class RegisterBookInput:
    title: str
    price: Optional[int] = None


RegisterBookInputType = InputType(
    "RegisterBookInput",
    lambda data: RegisterBookInput(**data),
)

MutationTypeインスタンスを作成する

QueryTypeと同様にMutationTypeというクラスのインスタンスを作成します。

from ariadne import MutationType

MutationType = MutationType()

ゾルバを定義する

クエリーの場合と同じようにリゾルバを実装して紐づけます。以下のように書くと、先ほど定義したRegisterBookInputを第3引数で受け取ることができます。

@MutationType.field("registerBook")
def resolve_register_book(
    _,
    info: GraphQLResolveInfo,
    input: RegisterBookInput,
):
    book = Book.objects.create(
        title=input.title,
        price=input.price,
        registered_by=info.context["request"].user,
    )
    return book

スキーマMutationInputを紐づける

最後にmake_executable_schemaの第二引数の配列に、以下のようにMutationTypeInput型を追加する必要があります。

schema = make_executable_schema(
    load_schema_from_path("schema.graphql"),
    [
        QueryType,
        MutationType,
        RegisterBookInputType,
    ],
)

これで以下のようにMutaionを実行できるようになります。

MutationTypeRegisterBookInputTypeの渡し方が分からず少しハマってしまい、以下のissueを読んで解決しました。

github.com

UserType#booksフィールドの実装

最後にUserTypeが持つbooksフィールドを実装します。

UserTypeを定義する

以下のissueによると、ルートではないクエリーを定義する場合は、ObjectTypeというクラスを使って、実装したいクエリーの所有者の型をプログラム上で宣言する必要があるそうです。

github.com

今回はbooksの所有者はUserTypeなので、UserTypeを宣言します。

from ariadne import ObjectType

# ...

UserType = ObjectType("User")

ゾルバを定義する

後は同じようにリゾルバを実装してUserTypeに紐づけます。ルートのクエリーの場合との違いとしては、第一引数で所有者のオブジェクトを受け取ることができるようです。

from django.db.models import QuerySet

# ...

@UserType.field("books")
def resolve_user_books(
    user: User, _: GraphQLResolveInfo
) -> QuerySet[Book]:
    return user.books.all()

こちらもQuerySetからBookTypeのリストへの変換はAriadneがやってくれました。

スキーマUserTypeを紐づける

同様にスキーマUserTypeを追加して紐づけます。

schema = make_executable_schema(
    load_schema_from_path("schema.graphql"),
    [
        QueryType,
        UserType,
        MutationType,
        RegisterBookInputType,
    ],
)

これで以下のようにネストしたクエリでbooksも取得できるようになりました。

以上となります。

感想

スキーマファーストということで、コードファースト系のライブラリよりも書かなければならないコード量はだいぶ多いのかなと思っていましたが、意外とAriadneがDjangoのModelからGraphQLの型への変換を自動でやってくれたため、少ないコード量で実装できました。

AriadneのDjangoのサポート状況が芳しくないということが分かったのは残念でしたが、スキーマファーストの思想のGraphQLライブラリを触ってみたことはいい経験になりました。