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
では、String
や Int
といったプリミティブな型と、独自の Actor
型の両方を使用していることに気がつくと思います。
もし、フィールドに型のリストを格納したい場合は、角括弧で囲みます – [Actor]
.
クエリーの作成
クエリとは、どのようなデータを取得できるのか、そのために何が必要なのかを指定するものです。
type Query {
actor(id: ID!): Actor
movie(id: ID!): Movie
actors: [Actor]
movies: [Movie]
}
この Query
タイプでは、Actor
と Movie
のデータを 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: ActorPayload
と MoviePayload
はミューテーションを成功させるために必要ではありませんが、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 クエリに対応します。
- Actor
と
movieプロパティは、それぞれ
ActorTypeと
MovieType` の値を返し、どちらも 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 の入力引数に対応します。 - ok
と
actorプロパティは
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では、フィールドを指定することができます。
ここでは、id
とname
を選択しました。
テストデータには映画が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
プロパティに対応していることに注意してください。
また、 ok
と actor
の戻り値が、 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 クエリを使ってテストしてみました。
完全なアプリケーションのソースコードをご覧になりたい方は、こちらをご覧ください。