はじめに
この記事は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をサポートしているのかが気になります。
結論からいうと、以下のような状態のようです。
公式の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のインストール
ariadne
とariadne-django
をインストールします。
pip install ariadne ariadne-django
INSTALLED_APPS
への追加
ariadne-django
のREADMEに記載の通り、INSTALLED_APPS
にariadne_django
を追加します。
INSTALLED_APPS = [
"ariadne_django",
"books",
]
ここがスキーマファーストのGraphQLを使う場合の特徴的な工程です。
DjangoのPJディレクトリ直下(=manage.py
と同じ階層)にschema.graphql
というファイルを作成します。
schema.graphql
に以下を記述していきます。
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
を紐づけます。
以下のように読み込んだスキーマファイルとQueryType
をmake_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を実装します。
まずは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
最後にmake_executable_schema
の第二引数の配列に、以下のようにMutationType
とInput
型を追加する必要があります。
schema = make_executable_schema(
load_schema_from_path("schema.graphql"),
[
QueryType,
MutationType,
RegisterBookInputType,
],
)
これで以下のようにMutaionを実行できるようになります。
※ MutationType
とRegisterBookInputType
の渡し方が分からず少しハマってしまい、以下の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ライブラリを触ってみたことはいい経験になりました。