17
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DjangoAdvent Calendar 2016

Day 19

Djangoでgraphene_djangoを使ってGraphQL API

Posted at

Djangoでgraphene_djangoを使ってGraphQL API

この記事は Django Advent Calendar 2016 19日目の記事です。

GraphQLのためのPython製フレームワークに graphene があります。grapheneにはO/R Mapperで使いやすくしたライブラリがいくつかあります。
今回はその中の1つ graphene-django を使ってみます。

インストール

venv環境を作ってactivateしておきましょう。

$ ~/ng/home/src/develop/pyvm/pythons/Python-3.5.2/bin/python3 -m venv env
$ source env/bin/activate
(env) $

pipでインストールします。

(env) $ pip install graphene_django
Collecting graphene-django
  Using cached graphene-django-1.2.1.tar.gz
Collecting six>=1.10.0 (from graphene-django)
Collecting graphene>=1.1.3 (from graphene-django)
  Using cached graphene-1.1.3.tar.gz
Collecting Django>=1.6.0 (from graphene-django)
  Using cached Django-1.10.4-py2.py3-none-any.whl
Collecting iso8601 (from graphene-django)
  Using cached iso8601-0.1.11-py2.py3-none-any.whl
Collecting singledispatch>=3.4.0.3 (from graphene-django)
  Using cached singledispatch-3.4.0.3-py2.py3-none-any.whl
Collecting graphql-core>=1.0.1 (from graphene>=1.1.3->graphene-django)
  Using cached graphql-core-1.0.1.tar.gz
Collecting graphql-relay>=0.4.5 (from graphene>=1.1.3->graphene-django)
  Using cached graphql-relay-0.4.5.tar.gz
Collecting promise>=1.0.1 (from graphene>=1.1.3->graphene-django)
  Using cached promise-1.0.1.tar.gz
Collecting typing (from promise>=1.0.1->graphene>=1.1.3->graphene-django)
  Using cached typing-3.5.2.2.tar.gz
Installing collected packages: six, typing, promise, graphql-core, graphql-relay, graphene, Django, iso8601, singledispatch, graphene-django
  Running setup.py install for typing ... done
  Running setup.py install for promise ... done
  Running setup.py install for graphql-core ... done
  Running setup.py install for graphql-relay ... done
  Running setup.py install for graphene ... done
  Running setup.py install for graphene-django ... done
Successfully installed Django-1.10.4 graphene-1.1.3 graphene-django-1.2.1 graphql-core-1.0.1 graphql-relay-0.4.5 iso8601-0.1.11 promise-1.0.1 singledispatch-3.4.0.3 six-1.10.0 typing-3.5.2.2

プロジェクトを作成する

django-admin startprojet でプロジェクトを作成します。
とりあえず myproj とかにしておきます。

(env) $ django-admin startproject myproj .

ディレクトリ構成はこんな感じです。

(env) $ tree myproj
myproj
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

schemaを定義する

myproj/schema.pyにAPIのスキーマを定義してみます。

import graphene
from graphene_django import DjangoObjectType
from django.contrib.auth import models as auth_models


class User(DjangoObjectType):
    class Meta:
        model = auth_models.User


class Query(graphene.ObjectType):
    users = graphene.List(User)

    @graphene.resolve_only_args
    def resolve_users(self):
        return auth_models.User.objects.all()


schema = graphene.Schema(query=Query)

モデルを作るのが面倒だったので django.contrib.auth.models.User() を使いました。

設定を追加する

graphene_djangoのための設定を追加していきます。

INSTALL_APPS に graphene_django を追加する

myproj/settings.py::

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',  # <- 追加
]

GRAPHENE にスキーマへのdotted nameを設定する

settings.pyに先ほど作成したschema.pyの中のschemaオブジェクトまでのdotted name (foo.bar.bazみたいなやつ) を指定します。

myproj/settings.py::

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

graphqlのリクエストを受け付けるためのURLを追加する

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

from graphene_django.views import GraphQLView  # <- 追加

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^graphql/', GraphQLView.as_view(graphiql=True)),  # <- 追加
]

http://localhost:8000/graphql/ がAPIのリクエストを受け付けるURLになります。
graphqlのリクエストを組みためのgraphiqlという画面が用意されています。
graphiql=True を指定すると有効になります。

起動する

migrate した後、起動してみましょう。

(env) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK
(env) $

起動します。

(env) $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 20, 2016 - 13:28:32
Django version 1.10.4, using settings 'myproj.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

ブラウザで http://localhost:8000/graphql/ にアクセスしてみましょう。

画面が表示されます。

取得する

データベースが空の状態のためユーザを作っておきます。

(env) $ python manage.py createsuperuser
Username (leave blank to use 'sximada'): foo
Email address: test@example.com
Password:
Password (again):
Superuser created successfully.

ではgraphiql画面でqueryを発行してみましょう。
左側のペインに以下のクエリを入力します。

query {
  users {
    id
    username
    email
    isSuperuser
        isStaff
  }
}

入力したら左上部にある再生マークをクリックします。
すると右側のペインに以下のような結果が表示されます。

{
  "data": {
    "users": [
      {
        "id": "1",
        "username": "foo",
        "email": "test@example.com",
        "isSuperuser": true,
        "isStaff": true
      }
    ]
  }
}

ユーザの情報が取得できました。ユーザが複数いる場合は以下のようになります。

{
  "data": {
    "users": [
      {
        "id": "1",
        "username": "foo",
        "email": "test@example.com",
        "isSuperuser": true,
        "isStaff": true
      },
      {
        "id": "2",
        "username": "bar",
        "email": "test2@example.com",
        "isSuperuser": true,
        "isStaff": true
      }
    ]
  }
}

proj.schema.Query.resolve_users()で全てのユーザを返しているので
全ユーザが一覧になって出力されます。

    @graphene.resolve_only_args
    def resolve_users(self):
        return auth_models.User.objects.all()  # <- ココ

id指定でユーザ指定して取得する

idを指定してユーザを指定したいので、
myproj/schema.py のQueryクラスを以下のように変更します。

myproj/schema.py::

class Query(graphene.ObjectType):
    user = graphene.Field(User, id=graphene.String())  # <- 追加
    users = graphene.List(User)

    @graphene.resolve_only_args                                # <- 追加
    def resolve_user(self, id):                                # <- 追加
        return auth_models.User.objects.filter(pk=id).first()  # <- 追加

    @graphene.resolve_only_args
    def resolve_users(self):
        return auth_models.User.objects.all()


開発サーバを再起動し、以下のqueryを実行します。

query {
  user(id: "1") {
    id
    username
    email
    isSuperuser
        isStaff
  }
}

実行すると次の結果が得られます。

{
  "data": {
    "user": {
      "id": "1",
      "username": "foo",
      "email": "test@example.com",
      "isSuperuser": true,
      "isStaff": true
    }
  }
}

今度はidで指定したユーザが取得できました。
もしemailが必要なければqueryからemailを削除してしまえばAPIサーバはemailを返しません。
どの情報を返して欲しいか (例えばemailが欲しいなど) をクライアント側で指定できるので
解析も楽になりますし、クライアント側の仕様変更で新しいfieldを取得したい場合にも
API側の修正をしなくてすみそうです。また必要のないデータのやり取りのしなくてすみます。

気をつけるところとしては アンダースコアのあるフィールド名が、
アンダースコアが省略されlowerキャメルケースになることです。

例)

  • auth_user.is_superuserisSuperuser
  • auth_user.is_staffiStaffr

存在しないidの場合は .first() でNoneになるのでnullになります。

クエリ::

query {
  user(id: "6589645936543") {
    id
    username
    email
    isSuperuser
    isStaff
  }
}

結果::

{
  "data": {
    "user": null
  }
}

両方を同時にリクエストすることもできる

クエリ::

query {
  user(id: "1") {
    id
    username
    email
    isSuperuser
    isStaff
  }
  users {
    id
    username
    lastLogin
  }
}

結果::

{
  "data": {
    "user": {
      "id": "1",
      "username": "foo",
      "email": "test@example.com",
      "isSuperuser": true,
      "isStaff": true
    },
    "users": [
      {
        "id": "1",
        "username": "foo",
        "lastLogin": null
      },
      {
        "id": "2",
        "username": "bar",
        "lastLogin": null
      }
    ]
  }
}

フィルターする

実際のアプリケーションなどではレコードを全て取得するよりも、
条件をつけてフィルターすることの方が多いでしょう。

先ほどのusersをfilterできるように書き換えます。

import graphene
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField  # <- 追加

from django.contrib.auth import models as auth_models


class User(DjangoObjectType):
    class Meta:
        model = auth_models.User
        filter_fields = ('username', 'email', 'is_staff')  # <- 追加
        interfaces = (graphene.relay.Node,)                # <- 追加


class Query(graphene.ObjectType):
    user = graphene.Field(User, id=graphene.String())
    users = DjangoFilterConnectionField(User)  # <- 変更

    @graphene.resolve_only_args
    def resolve_user(self, id):
        return auth_models.User.objects.filter(pk=id).first()

    # resolve_users()メソッドは削除

schema = graphene.Schema(query=Query)

filter_fields にモデルの属性名を指定します。
ここで指定した属性でフィルターできます。

開発サーバを再起動して次のクエリを実行します。

query {
  users(isStaff: true) {
    edges {
      node {
        username
        email
        isStaff
      }
    }
  }
}

isStaff: true と指定しています。スタッフ属性がついているユーザだけが返却されます。

{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "username": "foo",
            "email": "test@example.com",
            "isStaff": true
          }
        },
        {
          "node": {
            "username": "bar",
            "email": "test2@example.com",
            "isStaff": true
          }
        }
      ]
    }
  }
}

foo ユーザのスタッフ属性を外すと以下の結果になります。

{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "username": "bar",
            "email": "test2@example.com",
            "isStaff": true
          }
        }
      ]
    }
  }
}

所管

軽く触ってみた感想ですが結構癖があるなあと感じました。
プロダクションで使うには、GraphQLについて知る必要があるのはもちろんですが、
grapheneの使い方、graphene_djangoの使い方も一通り押さえておかないと
ハマって抜け出せないとう状況に陥りそうです。

ただ使いどころによってはすごく便利だなあとも思いました。
APIを何発も撃って表示していたところを1リクエストで済むので良いです。
GraphQLはfacebookが公開した仕様でフロントエンドではRelayで使うことができます。
そっちも合わせてもうちょっと遊んでみたい気にはなりましたね。

17
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?