DjangoでGraphQL APIを構築する

Web API は、今日の私たちのアプリケーションのほとんどを動かすエンジンです。

長年にわたり、RESTがAPIの主要なアーキテクチャでしたが、この記事ではGraphQLを探求していきます。

REST APIでは、一般的に、アクセス可能なデータの各オブジェクトにURLを作成します。

例えば、映画のREST APIを作るとしましょう。

映画そのもの、俳優、賞、監督、プロデューサー…すでに扱いにくくなっていますね。

これでは、1つの関連データに対して多くのリクエストが発生することになりかねません。

あなたが低速のインターネット接続で低性能の携帯電話のユーザーであったと想像してください。

この状況は理想的ではありません。

GraphQLはRESTのようなAPIアーキテクチャではなく、もっと簡単な方法で関連データを共有できるようにする言語なのです。

それを使って、映画のAPIを設計してみます。

その後、Djangoで映画のAPIを作ることで、GrapheneライブラリがどのようにPythonでAPIを構築することを可能にするかを見ていきます。

GraphQLとは

GraphQLは、元々Facebookによって作られましたが、現在はGraphQL Foundationの下で開発されています。

GraphQLは、データの取得と操作を可能にするクエリー言語とサーバーランタイムです。

APIで利用したいデータを定義するために、GraphQLの強い型付けシステムを利用します。

そして、API用のスキーマ(データを取得・変更するために許可されるクエリのセット)を作成します。

ムービースキーマの設計

タイプ作成

型は、APIで利用可能なデータの種類を記述します。

すでに提供されている基本的な型を利用することもできますが、独自の型を定義することも可能です。

例えば、俳優や映画に関する以下のような型を考えてみましょう。

type Actor {
  id: ID!
  name: String!
}


type Movie {
  id: ID!
  title: String!
  actors: [Actor]
  year: Int!
}


ID型は、そのフィールドがそのデータ型に対して一意な識別子であることを示します。

ID が文字列でない場合、この型は文字列に直列化される必要があります。

注:感嘆符は、そのフィールドが必須であることを意味します。

また、Movie では、StringInt といったプリミティブな型と、独自の Actor 型の両方を使用していることに気がつくと思います。

もし、フィールドに型のリストを格納したい場合は、角括弧で囲みます – [Actor].

クエリーの作成

クエリとは、どのようなデータを取得できるのか、そのために何が必要なのかを指定するものです。

type Query {
  actor(id: ID!): Actor
  movie(id: ID!): Movie
  actors: [Actor]
  movies: [Movie]
}


この Query タイプでは、ActorMovie のデータを ID を指定して取得したり、フィルタリングなしでリストを取得したりすることができます。

突然変異を起こす

変異は、サーバー上のデータを変更するためにどのような操作を行うことができるかを記述します。

ミューテーションは2つのものに依存しています。

  • 入力 – 個々のフィールドではなくオブジェクト全体を渡したい場合にのみミューテーションの引数として使用される特殊な型です。
  • Payloads – 通常の型ですが、APIが進化しても簡単に拡張できるように、慣習的にミューテーションの出力として使用します。

最初に行うことは、入力タイプを作成することです。

input ActorInput {
  id: ID
  name: String!
}


input MovieInput {
  id: ID
  title: String
  actors: [ActorInput]
  year: Int
}


そして、ペイロードの型を作成します。

type ActorPayload {
  ok: Boolean
  actor: Actor
}


type MoviePayload {
  ok: Boolean
  movie: Movie
}


OK` フィールドに注目してください。

ペイロード タイプにはステータスやエラー フィールドのようなメタデータが含まれるのが一般的です。

Mutation` タイプは、これらすべてをまとめています。

type Mutation {
  createActor(input: ActorInput) : ActorPayload
  createMovie(input: MovieInput) : MoviePayload
  updateActor(id: ID!, input: ActorInput) : ActorPayload
  updateMovie(id: ID!, input: MovieInput) : MoviePayload
}


createActorミューテーターはActorInput` オブジェクトを必要とし、そのオブジェクトにはアクターの名前が必要です。

updateActorミューテーターは更新されるアクターのID` と更新された情報を必要とします。

ムービーミューテーターも同様です。

Note: ActorPayloadMoviePayload はミューテーションを成功させるために必要ではありませんが、API がアクションを処理する際にフィードバックを提供することは良い習慣です。

スキーマの定義

最後に、作成したクエリーとミューテーションをスキーマに対応させます。

schema {
  query: Query
  mutation: Mutation
}


Graphene Libraryの使用方法

GraphQLはプラットフォームに依存しないので、様々なプログラミング言語(Java、PHP、Go)、フレームワーク(Node.js、Symfony、Rails)、またはApolloのようなプラットフォームでGraphQLサーバーを作成することができる。

Grapheneを使えば、スキーマを作成するためにGraphQLの構文を使う必要はなく、Pythonを使うだけです このオープンソースライブラリはDjangoにも統合されており、アプリケーションのモデルを参照することでスキーマを作成することができます。

アプリケーションセットアップ

仮想環境

Django プロジェクトでは、仮想環境を作成するのがベストプラクティスと考えられてい ます。

Python 3.6 以降、 venv モジュールが仮想環境の作成と管理のために含まれています。

ターミナルを使って、ワークスペースに入り、以下のフォルダを作成します。

$ mkdir django_graphql_movies
$ cd django_graphql_movies/


次に、仮想環境を作成します。

$ python3 -m venv env


ディレクトリの中に新しい env フォルダが作成されるはずです。

Pythonのパッケージをインストールするときに、システム全体ではなく、このプロジェクトだけで利用できるようにするために、仮想環境を有効にする必要があります。

$ . env/bin/activate


Note: 仮想環境を終了して通常のシェルを使用するには、deactivateと入力してください。

これはチュートリアルの最後に行う必要があります。

DjangoとGrapheneのインストールと設定

仮想環境にいる間に、pip を使って Django と Graphene ライブラリをインストールします。

$ pip install Django
$ pip install graphene_django


そして、Django プロジェクトを作成します。

$ django-admin.py startproject django_graphql_movies .


Django のプロジェクトは多くのアプリから構成されます。

アプリはプロジェクト内で再利用可能なコンポーネントであり、アプリでプロジェクトを作成するのがベストプラクティスです。

では、ムービー用のアプリを作ってみましょう。

$ cd django_graphql_movies/
$ django-admin.py startapp movies


アプリケーションを動作させる前に、データベースを同期させましょう。

# First return to the project's directory
$ cd ..
# And then run the migrate command
$ python manage.py migrate


モデルの作成

Django のモデルは、私たちのプロジェクトのデータベースのレイアウトを記述します。

各モデルは Python のクラスで、通常データベースのテーブルにマップされます。

クラスのプロパティはデータベースのカラムにマップされます。

以下のコードを django_graphql_movies/movies/models.py に打ち込んでください。

from django.db import models


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


def __str__(self):
        return self.name


class Meta:
        ordering = ('name',)


class Movie(models.Model):
    title = models.CharField(max_length=100)
    actors = models.ManyToManyField(Actor)
    year = models.IntegerField()


def __str__(self):
        return self.title


class Meta:
        ordering = ('title',)


GraphQL スキーマと同様に、Actor モデルには名前があり、Movie モデルにはタイトル、俳優との多対多の関係、そして年があります。

ID は Django が自動的に生成してくれます。

これで、映画アプリをプロジェクトに登録することができます。

django_graphql_movies/settings.pyに移動して、INSTALLED_APPS` を以下のように変更してください。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_graphql_movies.movies',
]


データベースをマイグレートして、私たちのコード変更と同期させてください。

$ python manage.py makemigrations
$ python manage.py migrate


テストデータの読み込み

APIを構築したら、クエリを実行して動作確認を行いたいものです。

以下のJSONを movies.json という名前でプロジェクトのルートディレクトリに保存してください。

[
  {
    "model": "movies.actor",
    "pk": 1,
    "fields": {
      "name": "Michael B. Jordan"
    }
  },
  {
    "model": "movies.actor",
    "pk": 2,
    "fields": {
      "name": "Sylvester Stallone"
    }
  },
  {
    "model": "movies.movie",
    "pk": 1,
    "fields": {
      "title": "Creed",
      "actors": [1, 2],
      "year": "2015"
    }
  }
]


そして、以下のコマンドを実行して、テストデータをロードします。

$ python manage.py loaddata movies.json


ターミナルに以下のような出力が表示されるはずです。

Installed 3 object(s) from 1 fixture(s)


Grapheneでスキーマを作成する

クエリーの作成

ムービーアプリのフォルダに schema.py ファイルを新規に作成し、GraphQL タイプを定義しましょう。

import graphene
from graphene_django.types import DjangoObjectType, ObjectType
from django_graphql_movies.movies.models import Actor, Movie


# Create a GraphQL type for the actor model
class ActorType(DjangoObjectType):
    class Meta:
        model = Actor


# Create a GraphQL type for the movie model
class MovieType(DjangoObjectType):
    class Meta:
        model = Movie


Graphene の助けを借りて、GraphQL タイプを作るには、どの Django モデルが API に必要なプロパティを持つかを指定するだけでよいのです。

同じファイルに、以下のコードを追加して Query タイプを作成します。

# Create a Query type
class Query(ObjectType):
    actor = graphene.Field(ActorType, id=graphene.Int())
    movie = graphene.Field(MovieType, id=graphene.Int())
    actors = graphene.List(ActorType)
    movies= graphene.List(MovieType)


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


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


return None


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


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


return None


def resolve_actors(self, info, **kwargs):
        return Actor.objects.all()


def resolve_movies(self, info, **kwargs):
        return Movie.objects.all()


Query` クラスの各プロパティは、GraphQL クエリに対応します。

  • Actormovieプロパティは、それぞれActorTypeMovieType` の値を返し、どちらも ID は整数である必要がある。
  • Actorsプロパティとmovies` プロパティは、それぞれの型のリストを返します。

Query クラスで作成した 4 つのメソッドは、リゾルバと呼ばれます。

リゾルバはスキーマのクエリを、データベースが行う実際の動作に接続します。

Django の標準として、我々はモデルを通してデータベースと対話します。

resolve_actor関数を考えてみましょう。

クエリパラメータから ID を取得し、その ID を主キーとするアクターをデータベースから返します。

resolve_actors 関数は、単純にデータベース内の全てのアクターを取得し、リストとして返します。

突然変異を起こす

スキーマを設計したとき、まず突然変異のための特別な入力型を作りました。

Grapheneでも同じように、schema.pyに追加してみましょう。

# Create Input Object Types
class ActorInput(graphene.InputObjectType):
    id = graphene.ID()
    name = graphene.String()


class MovieInput(graphene.InputObjectType):
    id = graphene.ID()
    title = graphene.String()
    actors = graphene.List(ActorInput)
    year = graphene.Int()


これは単純なクラスで、APIのデータを変更するためにどのフィールドを使用できるかを定義します。

ミューテーションを作成するには、クエリを作成するよりも少し多くの作業が必要です。

それでは、アクター用のミューテーションを追加してみましょう。

# Create mutations for actors
class CreateActor(graphene.Mutation):
    class Arguments:
        input = ActorInput(required=True)


ok = graphene.Boolean()
    actor = graphene.Field(ActorType)


@staticmethod
    def mutate(root, info, input=None):
        ok = True
        actor_instance = Actor(name=input.name)
        actor_instance.save()
        return CreateActor(ok=ok, actor=actor_instance)


class UpdateActor(graphene.Mutation):
    class Arguments:
        id = graphene.Int(required=True)
        input = ActorInput(required=True)


ok = graphene.Boolean()
    actor = graphene.Field(ActorType)


@staticmethod
    def mutate(root, info, id, input=None):
        ok = False
        actor_instance = Actor.objects.get(pk=id)
        if actor_instance:
            ok = True
            actor_instance.name = input.name
            actor_instance.save()
            return UpdateActor(ok=ok, actor=actor_instance)
        return UpdateActor(ok=ok, actor=None)


スキーマを設計したときに作成した createActor 変異のシグネチャを思い出してください。

createActor(input: ActorInput) : ActorPayload


  • このクラスの名前は GraphQL のクエリ名に対応しています。
  • 内側の Arguments クラスのプロパティは mutator の入力引数に対応します。
  • okactorプロパティはActorPayload` を構成しています。

mutation` メソッドを書くときに知っておくべき重要なことは、Django のモデルにデータを保存することです。

  • 入力オブジェクトから名前を取得し、新しい Actor オブジェクトを作成します。
    入力オブジェクトから名前を取得し、新しい Actor オブジェクトを作成します。 * save 関数を呼び出して、データベースを更新し、ペイロードをユーザに返します。

UpdateActor` クラスは、更新されるアクターを取得し、保存する前にそのプロパティを変更するロジックを追加して、同様のセットアップを行っています。

それでは、ムービー用のミューテーションを追加してみましょう。

# Create mutations for movies
class CreateMovie(graphene.Mutation):
    class Arguments:
        input = MovieInput(required=True)


ok = graphene.Boolean()
    movie = graphene.Field(MovieType)


@staticmethod
    def mutate(root, info, input=None):
        ok = True
        actors = []
        for actor_input in input.actors:
          actor = Actor.objects.get(pk=actor_input.id)
          if actor is None:
            return CreateMovie(ok=False, movie=None)
          actors.append(actor)
        movie_instance = Movie(
          title=input.title,
          year=input.year
          )
        movie_instance.save()
        movie_instance.actors.set(actors)
        return CreateMovie(ok=ok, movie=movie_instance)


class UpdateMovie(graphene.Mutation):
    class Arguments:
        id = graphene.Int(required=True)
        input = MovieInput(required=True)


ok = graphene.Boolean()
    movie = graphene.Field(MovieType)


@staticmethod
    def mutate(root, info, id, input=None):
        ok = False
        movie_instance = Movie.objects.get(pk=id)
        if movie_instance:
            ok = True
            actors = []
            for actor_input in input.actors:
              actor = Actor.objects.get(pk=actor_input.id)
              if actor is None:
                return UpdateMovie(ok=False, movie=None)
              actors.append(actor)
            movie_instance.title=input.title
            movie_instance.year=input.year
            movie_instance.save()
            movie_instance.actors.set(actors)
            return UpdateMovie(ok=ok, movie=movie_instance)
        return UpdateMovie(ok=ok, movie=None)


ムービーはアクターを参照するので、保存する前にデータベースからアクターデータを取得する必要があります。

for` ループは、まずユーザーから提供されたアクターが本当にデータベースにあるかどうかを確認し、なければデータを保存せずに返します。

Django で多対多の関係を扱う場合、関連するデータを保存できるのは、オブジェクトが保存された後だけです。

これが、 movie_instance.save() でムービーを保存してから、 movie_instance.actors.set(actors) で俳優を設定する理由です。

Mutationを完成させるために、Mutationタイプを作成します。

class Mutation(graphene.ObjectType):
    create_actor = CreateActor.Field()
    update_actor = UpdateActor.Field()
    create_movie = CreateMovie.Field()
    update_movie = UpdateMovie.Field()


スキーマの作成

スキーマを設計したときと同じように、クエリとミューテーションをアプリケーションの API に対応させます。

これを schema.py の末尾に追加します。

schema = graphene.Schema(query=Query, mutation=Mutation)


スキーマをプロジェクトに登録する

API を動作させるために、プロジェクト全体でスキーマを利用できるようにする必要があります。

django_graphql_movies/schema.py` ファイルを新規に作成し、以下を追加してください。

import graphene
import django_graphql_movies.movies.schema


class Query(django_graphql_movies.movies.schema.Query, graphene.ObjectType):
    # This class will inherit from multiple Queries
    # as we begin to add more apps to our project
    pass


class Mutation(django_graphql_movies.movies.schema.Mutation, graphene.ObjectType):
    # This class will inherit from multiple Queries
    # as we begin to add more apps to our project
    pass


schema = graphene.Schema(query=Query, mutation=Mutation)


ここからgrapheneを登録し、我々のスキーマを使用するように指示します。

django_graphql_movies/settings.pyを開き、INSTALLED_APPSの最初の項目として‘graphene_django’,` を追加する。

同じファイルで、INSTALLED_APPS の下に以下のコードを数行追加してください。

GRAPHENE = {
    'SCHEMA': 'django_graphql_movies.schema.schema'
}


GraphQL APIは、/graphqlという1つのエンドポイントを経由してアクセスします。

そのルートというか、ビューを Django に登録する必要があります。

django_graphql_movies/urls.py` を開いて、ファイルの中身を以下のように変更します。

from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema


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


API のテスト

APIをテストするために、プロジェクトを実行し、GraphQLエンドポイントに移動してみましょう。

ターミナルで次のように入力します。

$ python manage.py runserver


サーバーが起動したら、http://127.0.0.1:8000/graphql/にアクセスしてください。

クエリを実行するためのIDEであるGraphiQLが表示されます。

クエリーの書き方

最初のクエリでは、データベース内のすべてのアクターを取得しましょう。

左上のペインに次のように入力します。

query getActors {
  actors {
    id
    name
  }
}


これは GraphQL のクエリのフォーマットです。

キーワード query で始まり、その後にオプションでクエリの名前を指定します。

クエリに名前を付けておくと、ロギングやデバッグの際に便利です。

GraphQLでは、フィールドを指定することができます。

ここでは、idnameを選択しました。

テストデータには映画が1つしかありませんが、movieクエリを試してみて、GraphQLの素晴らしい機能をまた1つ発見してください。

query getMovie {
  movie(id: 1) {
    id
    title
    actors {
      id
      name
    }
  }
}


movieクエリには ID が必要なので、括弧内に ID を記述します。

興味深いのはactorsフィールドです。

Django のモデルでは、Movieクラスにactorsプロパティを含め、両者の間に多対多のリレーションシップを指定しています。

これにより、ムービーデータに関連するActor` 型のプロパティを全て取得することができます。

このグラフのようなデータの走査が、GraphQL が強力でエキサイティングな技術であると考えられている大きな理由なのです

ライティング・ミューテーション

ミューテーションはクエリと同じようなスタイルで記述します。

データベースにアクターを追加してみましょう。

mutation createActor {
  createActor(input: {
    name: "Tom Hanks"
  }) {
    ok
    actor {
      id
      name
    }
  }
}


パラメータ input が、先ほど作成した Arguments クラスの input プロパティに対応していることに注意してください。

また、 okactor の戻り値が、 CreateActor 変異のクラスプロパティに対応していることにも注意してください。

これで、トム・ハンクスが出演した映画を追加することができます。

mutation createMovie {
  createMovie(input: {
    title: "Cast Away",
    actors: [
      {
        id: 3
      }
    ]
    year: 1999
  }) {
    ok
    movie{
      id
      title
      actors {
        id
        name
      }
      year
    }
  }
}


残念ながら、これは間違いです。

更新クエリを実行して修正しましょう。

mutation updateMovie {
  updateMovie(id: 2, input: {
    title: "Cast Away",
    actors: [
      {
        id: 3
      }
    ]
    year: 2000
  }) {
    ok
    movie{
      id
      title
      actors {
        id
        name
      }
      year
    }
  }
}


これで、すべて解決です!

POSTで通信する

GraphiQLは開発中にとても便利ですが、外部の開発者がAPIを見すぎてしまう可能性があるので、実運用ではこのビューを無効にするのが標準的なやり方です。

GraphiQLを無効にするには、 django_graphql_movies/urls.py を編集して、 path('graphql/', GraphQLView.as_view(graphiql=True)),path('graphql/', GraphQLView.as_view(graphiql=False)), にするだけでよいでしょう。

API と通信するアプリケーションは /graphql のエンドポイントに POST リクエストを送信することになる。

Django サイトの外から POST リクエストをする前に、 django_graphql_movies/urls.py を変更する必要があります。

from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema
from django.views.decorators.csrf import csrf_exempt # New library


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


Django には CSRF (Cross-Site Request Forgery) 保護機能が組み込まれてい ます – 不正に認証されたサイトのユーザが悪意のある行動を取るのを防ぐための手段です。

これは有用な保護機能ですが、外部アプリケーションが API と通信するのを防いでしまいます。

アプリケーションを実稼働させる場合は、他の認証方法を検討する必要があります。

ターミナルで次のように入力し、すべてのアクターを取得します。

$ curl 
  -X POST 
  -H "Content-Type: application/json" 
  --data '{ "query": "{ actors { name } }" }' 
  http://127.0.0.1:8000/graphql/


次のように表示されるはずです。

{"data":{"actors":[{"name":"Michael B. Jordan"},{"name":"Sylvester Stallone"},{"name":"Tom Hanks"}]}}


結論

GraphQLは、進化可能なAPIを作成するのに役立つ、強い型付けのクエリ言語です。

私たちは映画のAPIスキーマを設計し、データの取得と変更に必要な型、クエリ、ミューテーションを作成しました。

Grapheneを使えば、Djangoを使ってGraphQL APIを作成することができます。

私たちは先に設計したムービースキーマを実装し、GraphiQL と標準の POST リクエストによる GraphQL クエリを使ってテストしてみました。

完全なアプリケーションのソースコードをご覧になりたい方は、こちらをご覧ください。

タイトルとURLをコピーしました