Djangoでgraphene_djangoを使ってGraphQL API

# Djangoでgraphene_djangoを使ってGraphQL API

この記事は [Django Advent Calendar 2016](http://qiita.com/advent-calendar/2016/django) 19日目の記事です。

GraphQLのためのPython製フレームワークに [graphene](http://graphene-python.org/) があります。grapheneにはO/R Mapperで使いやすくしたライブラリがいくつかあります。
今回はその中の1つ [graphene-django](https://github.com/graphql-python/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_superuser` → `isSuperuser`
- `auth_user.is_staff` → `iStaffr`


存在しない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で使うことができます。
そっちも合わせてもうちょっと遊んでみたい気にはなりましたね。