はてなの金次郎

Pythonエンジニアの技術系ブログ

DjangoでGraphQLを実装する【Query編】

はじめに

Django Advent Calendar 2018 の 2日目の記事です。

qiita.com

RESTの次のパラダイムとして注目されているGraphQL。
本記事はDjangoでGraphQLを実装する方法を紹介します。

GraphQLの動向

GraphQLFacebookによって開発されたOSSで、Web APIのクエリ言語です。

graphql.org

GitHubAPIに採用されたり、AWSのフルマネージドGraphQLサービス AppSyncが発表されたりなど徐々に盛り上がりを見せているように思います。

参考:2018年のAPI動向 - ポストREST時代の到来? / gihyo.jp

また、今年で3年目になるGraphQL Summit 2018では850人以上のエンジニアが集まり盛り上がりを見せたようです。

参考:GraphQL Summit 2018 に参加してきました / Mercari Engineering Blog

Python / Django界隈におけるGraphQLの動向

上記のようにGraphQLに関連したトークチュートリアルが多く確認できました。

このような注目度やGraphQLの理解を深めるといった目的から自分の得意としている言語とフレームワークでGraphQLを実装してみようと思ったのが動機です。

Graphene

GrapheneはGraphQLフレームワークPyPIgrapheneとして登録されています。

graphene-python.org

また、同じ組織がDjangoでGrapheneを利用するためのライブラリgraphene-django を公開しています。

今回、graphene-djangoを利用してDjangoでGraphQLを実装してみたいと思います。

GraphQL APIを作成する

Graphene-DjangoチュートリアルをベースにGraphQL APIを作成してみます。
作成するAPIは食材の名前やカテゴリに関するAPIです。

Graphene-Djangoチュートリアルをやる前に

に目を通しておくといいみたいです。

説明に必要な部分だけ抜粋しますので詳細はGraphene-Djangoチュートリアルをご確認ください。

モデル

材料モデルとそれにリーレーションを持つカテゴリモデルを定義します。
ここではGraphene特有の書き方はありません。

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Ingredient(models.Model):
    name = models.CharField(max_length=100)
    notes = models.TextField()
    category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)

    def __str__(self):
        return self.name

ルーティング

GraphQLのエンドポイントを定義します。エンドポイントは基本的に1つです。

graphiql=True とするとGraphiQLというVisual Editorが使えるようになります。

from django.conf.urls import path
from django.contrib import admin

from graphene_django.views import GraphQLView

from cookbook.schema import schema

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', GraphQLView.as_view(graphiql=True, schema=schema)),
]

GraphQLスキーマ

A GraphQL schema describes your data model, and provides a GraphQL server with an associated set of resolve methods that know how to fetch data.

FYI: https://docs.graphene-python.org/en/latest/quickstart/#creating-a-basic-schema

GraphQLスキーマはデータのフィールドや型などデータを取得するための情報を定義するデータセットです。

クエリが入力されると定義したスキーマに対してクエリが検証され、実行されます。
RESTにおけるスキーマとほぼ同等の役割と認識しています。

GraphQLではデータに対する操作は QueryMutation の二つに分類されるみたいです。
Query はデータの取得で、Mutation はデータの登録や更新です。

Query

データの取得を実装します。
アプリケーション下のディレクトリに作成した schema.py にQueryを定義します。

全件取得

import graphene

from graphene_django.types import DjangoObjectType

from .models import Category, Ingredient


class CategoryType(DjangoObjectType):
    class Meta:
        model = Category


class IngredientType(DjangoObjectType):
    class Meta:
        model = Ingredient


class Query:
    all_categories = graphene.List(CategoryType)
    all_ingredients = graphene.List(IngredientType)

    def resolve_all_categories(self, info, **kwargs):
        return Category.objects.all()

    def resolve_all_ingredients(self, info, **kwargs):
        return Ingredient.objects.select_related('category').all()

GraphiQLで Ingredient のデータを全件取得するクエリを実際に叩いてみます。

※ 検証前にフィクスチャでデータを事前にロードしておく必要があります。

f:id:gyuuuutan:20181202215724p:plain
全件取得のクエリ

リレーションをもつ全件取得もできます。

f:id:gyuuuutan:20181202221239p:plain
リレーションをもつ全件取得

1件取得

# ...

class Query(object):
    category = graphene.Field(CategoryType,
                              id=graphene.Int(),
                              name=graphene.String())
    all_categories = graphene.List(CategoryType)


    ingredient = graphene.Field(IngredientType,
                                id=graphene.Int(),
                                name=graphene.String())
    all_ingredients = graphene.List(IngredientType)

    def resolve_all_categories(self, info, **kwargs):
        return Category.objects.all()

    def resolve_all_ingredients(self, info, **kwargs):
        return Ingredient.objects.all()

    def resolve_category(self, info, **kwargs):
        id = kwargs.get('id')
        name = kwargs.get('name')

        if id is not None:
            return Category.objects.get(pk=id)

        if name is not None:
            return Category.objects.get(name=name)

        return None

    def resolve_ingredient(self, info, **kwargs):
        id = kwargs.get('id')
        name = kwargs.get('name')

        if id is not None:
            return Ingredient.objects.get(pk=id)

        if name is not None:
            return Ingredient.objects.get(name=name)

        return None

GraphiQLでクエリを実際に叩いてみます。

f:id:gyuuuutan:20181202223201p:plain
1件取得のクエリ

指定した idname でオブジェクトが取得できていることがわかります。

複雑な取得

Relaydjango-filter を活用することでより複雑なクエリでオブジェクトを取得できたり、実装をシンプルにしたりすることができるようです。

from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField

from .models import Category, Ingredient


class CategoryNode(DjangoObjectType):
    class Meta:
        model = Category
        filter_fields = ['name', 'ingredients']
        interfaces = (relay.Node, )


class IngredientNode(DjangoObjectType):
    class Meta:
        model = Ingredient
        filter_fields = {
            'name': ['exact', 'icontains', 'istartswith'],
            'notes': ['exact', 'icontains'],
            'category': ['exact'],
            'category__name': ['exact'],
        }
        interfaces = (relay.Node, )


class Query(object):
    category = relay.Node.Field(CategoryNode)
    all_categories = DjangoFilterConnectionField(CategoryNode)

    ingredient = relay.Node.Field(IngredientNode)
    all_ingredients = DjangoFilterConnectionField(IngredientNode)

GraphiQLで実際にクエリを叩いてみます。

f:id:gyuuuutan:20181202224048p:plain
複雑な取得のクエリ

所感

エンドポイントを一つ用意するだけでデータの取得ができてしまうのは色々恩恵がありそうです。
リクエスト数を削減できたり、必要なフィールドのみを取得できたり柔軟性が高いのもGraphQLの特徴だと実感することができました。

クエリの書き方は若干癖がありますが、かなり直感的ではあるので一度書き方を覚えてしまえばそこまで苦労はしなそうです。

ただ、サーバー側が楽になった分クライアント側の実装者の負担が増えるのかなと感じました。

RESTの場合はドキュメント通りに叩けばドキュメント通りに結果を返してくれますが(そうではないAPIもありますが)、GraphQLの場合はクライアントの実装者が適切なクエリを考えなければならないので、データベースやクエリ言語に対する一定の理解が必要になりそうです。

k0kubun.hatenablog.com

workplus.feedforce.jp

おわりに

1つの記事でまとめたかったのですがかなりのボリュームになってしまうので、3部構成で書きたいと思います。

現時点では以下の構成で書く予定です。

  1. DjangoでGraphQLを実装する【Query編】:本記事
  2. DjangoでGraphQLを実装する【Mutation編】:公開予定
  3. DjangoでGraphQLを実装する【認証と認可編】:公開予定

次のステップとして考えていることはApolloでGraphQLを実装することです。

今回はPython/DjangoでGraphQLを実装しましたが、GraphQLの特性上、サーバーとクライアントを一緒に実装するのがよいと思っています。

Apolloはクライアント側のライブラリが充実しているようです。

どこかの機会で挑戦してみたいです。

www.kabuku.co.jp