こんにちは。KOUKIです。
GraphQLの実装の続きです。前回は、Mutationの実装方法について触れました。
今回は、JWTとReactの実装について学びます。
<目次>
記事まとめ
Django GraphQL JWT
Django GraphQL JWTで認証機能を付けます。
まずは、settings.pyに次の設定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
GRAPHENE = { 'SCHEMA': 'app.schema.schema', # https://django-graphql-jwt.domake.io/en/latest/quickstart.html 'MIDDLEWARE': [ 'graphql_jwt.middleware.JSONWebTokenMiddleware', ], } MIDDLEWARE = [ ... ] AUTHENTICATION_BACKENDS = [ 'graphql_jwt.backends.JSONWebTokenBackend', 'django.contrib.auth.backends.ModelBackend', ] |
次にapp/api/schema.py配下に次の設定をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# app/api/schema.py import graphene from graphene_django.types import DjangoObjectType from .models import Author, Book import graphql_jwt # new class BookType(DjangoObjectType): class Meta: model = Book book_new = graphene.String() def resolve_book_new(self, info): return "Old Book" if self.publish < 1999 else "New Book" class AuthorType(DjangoObjectType): class Meta: model = Author class Query(graphene.ObjectType): all_books = graphene.List(BookType) book = graphene.Field(BookType, id=graphene.Int(), title=graphene.String()) all_authors = graphene.List(AuthorType) def resolve_all_authors(self, info, **kwargs): return Author.objects.all() def resolve_all_books(self, info, **kwargs): return Book.objects.all() def resolve_book(self, info, **kwargs): id = kwargs.get('id') title = kwargs.get('title') if id is not None: return Book.objects.get(pk=id) if title is not None: return Book.objects.get(title=title) return None class BookCreateMutation(graphene.Mutation): class Arguments: title = graphene.String(required=True) publish = graphene.Int() book = graphene.Field(BookType) def mutate(self, info, title, publish): book = Book.objects.create(title=title, publish=publish) return BookCreateMutation(book=book) class BookUpdateMutation(graphene.Mutation): class Arguments: title = graphene.String() publish = graphene.Int() id = graphene.ID(required=True) book = graphene.Field(BookType) def mutate(self, info, id, title, publish): book = Book.objects.get(pk=id) if title is not None: book.title = title if publish is not None: book.publish = publish book.save() return BookUpdateMutation(book=book) class BookDeleteMutation(graphene.Mutation): class Arguments: id = graphene.ID(required=True) book = graphene.Field(BookType) def mutate(self, info, id): book = Book.objects.get(pk=id) book.delete() return BookDeleteMutation(book=None) class Mutation: create_book = BookCreateMutation.Field() update_book = BookUpdateMutation.Field() delete_book = BookDeleteMutation.Field() token_auth = graphql_jwt.ObtainJSONWebToken.Field() # new |
Mutationにtokenを登録しました。
Mutationを発行して、ユーザーのTokenが取得できるか確認してください。
ユーザーを作成していない場合は、次のコマンドで作成することができます。
1 |
make admin |
ブラウザをリロードして、次のMutationを実行してください。
1 2 3 4 5 |
mutation { tokenAuth(username: "admin", password: "XXXX") { token } } |
1 2 3 4 5 6 7 |
{ "data": { "tokenAuth": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc0MTM2NDU1LCJvcmlnSWF0IjoxNTc0MTM2MTU1fQ.njgOsvhsAhpQWy8" } } } |
ログイン設定
ログインの設定には2パターンあります。
1パターン目
resolve_all_books関数などには、”info”引数を指定していました。
ここには、context情報が入っており、その中にはuser情報が含まれます。
つまり、次のようにログイン機能を設定できます。
1 2 3 4 5 |
def resolve_all_authors(self, info, **kwargs): user = info.context.user if not user.is_authenticated: raise Exception("Auth credentials were not provided!!!") return Author.objects.all() |
2パターン目
もう一つは、JWTモジュールの機能を利用するパターンです。こちらの方がすっきりとした実装ができます。
1 2 3 4 5 6 7 8 9 |
from graphql_jwt.decorators import login_required @login_required def resolve_all_authors(self, info, **kwargs): # user = info.context.user # if not user.is_authenticated: # raise Exception("Auth credentials were not provided!!!") return Author.objects.all() ... |
認証機能の実装
続いて、認証機能を実装していきましょう。
まずは、settings.pyに次の設定を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# settings.py from datetime import timedelta GRAPHQL_JWT = { 'JWT_VERIFY_EXPIRATION': True, 'JWT_EXPIRATION_DELTA': timedelta(minutes=5), 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7), } MIDDLEWARE = [ … ] |
上記は、認証時間等の設定です。
次にapp/urls.pyに次の設定を追加します。
1 2 3 4 5 6 7 8 9 10 11 |
# app/app/urls.py from django.contrib import admin from django.urls import path from graphene_django.views import GraphQLView from graphql_jwt.decorators import jwt_cookie urlpatterns = [ path('admin/', admin.site.urls), path('graphql/', jwt_cookie(GraphQLView.as_view(graphiql=True))), ] |
最後にapp/api/schema.pyに次の設定を追加します。
1 2 3 4 5 6 7 8 |
# app/api/schema.py class Mutation: create_book = BookCreateMutation.Field() update_book = BookUpdateMutation.Field() delete_book = BookDeleteMutation.Field() token_auth = graphql_jwt.ObtainJSONWebToken.Field() verify_token = graphql_jwt.Verify.Field() # new |
ブラウザをリロードして動作確認をしましょう。
この機能の確認には、Tokenが必要なので、先にTokenを取得してから動作確認をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Tokenを取得 mutation GetToken{ tokenAuth(username: "admin", password: "XXXX") { token } } # 認証 mutation VerifyToken { verifyToken(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc0MTM3NDU3LCJvcmlnSWF0IjoxNTc0MTM3MTU3fQ.mCSUWgxrW4p7E-dwB0h_96goWu6VpTDL42xF70qD31k") { payload } } |
1 2 3 4 5 6 7 8 9 10 11 |
{ "data": { "verifyToken": { "payload": { "username": "admin", "exp": 1574137457, "origIat": 1574137157 } } } } |
認証機能の実装も簡単ですね。
RelayとFilterの実装
Relayを実装します。
Relayは、データ駆動型のReactアプリケーションを開発するためのJavaScriptのフレームワークです。
Reactと同様にFacebookにて開発されています。
サーバーから取得したデータをReactコンポーネントにprops経由で渡すことができるようになるなど、かなり強力なツールです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
import graphene from graphene import relay # new from graphene_django.types import DjangoObjectType from .models import Author, Book import graphql_jwt from graphql_jwt.decorators import login_required from graphene_django.filter import DjangoFilterConnectionField # new class BookType(DjangoObjectType): class Meta: model = Book book_new = graphene.String() def resolve_book_new(self, info): return "Old Book" if self.publish < 1999 else "New Book" class AuthorType(DjangoObjectType): class Meta: model = Author # new class BookNode(DjangoObjectType): class Meta: model = Book filter_fields = ['title', 'publish'] interfaces = (relay.Node,) class Query(graphene.ObjectType): all_books = DjangoFilterConnectionField(BookNode) book = graphene.Field(BookType, id=graphene.Int(), title=graphene.String()) all_authors = graphene.List(AuthorType) @login_required def resolve_all_authors(self, info, **kwargs): # user = info.context.user # if not user.is_authenticated: # raise Exception("Auth credentials were not provided!!!") return Author.objects.all() # comment out # def resolve_all_books(self, info, **kwargs): # return Book.objects.all() def resolve_book(self, info, **kwargs): id = kwargs.get('id') title = kwargs.get('title') if id is not None: return Book.objects.get(pk=id) if title is not None: return Book.objects.get(title=title) return None class BookCreateMutation(graphene.Mutation): class Arguments: title = graphene.String(required=True) publish = graphene.Int() book = graphene.Field(BookType) def mutate(self, info, title, publish): book = Book.objects.create(title=title, publish=publish) return BookCreateMutation(book=book) class BookUpdateMutation(graphene.Mutation): class Arguments: title = graphene.String() publish = graphene.Int() id = graphene.ID(required=True) book = graphene.Field(BookType) def mutate(self, info, id, title, publish): book = Book.objects.get(pk=id) if title is not None: book.title = title if publish is not None: book.publish = publish book.save() return BookUpdateMutation(book=book) class BookDeleteMutation(graphene.Mutation): class Arguments: id = graphene.ID(required=True) book = graphene.Field(BookType) def mutate(self, info, id): book = Book.objects.get(pk=id) book.delete() return BookDeleteMutation(book=None) class Mutation: create_book = BookCreateMutation.Field() update_book = BookUpdateMutation.Field() delete_book = BookDeleteMutation.Field() token_auth = graphql_jwt.ObtainJSONWebToken.Field() verify_token = graphql_jwt.Verify.Field() |
新たにBookNodeクラスを作成し、Queryに設定したresolve_all_booksをコメントアウトしました。
ブラウザをリロードして、次のクエリを実行してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
query AllBook { allBooks { edges { node { id title author { id firstname lastname } } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
{ "data": { "allBooks": { "edges": [ { "node": { "id": "Qm9va05vZGU6MQ==", "title": "Harry Potter and the Philosopher's Stone", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } }, { "node": { "id": "Qm9va05vZGU6Mg==", "title": "Harry Potter and the Chamber of Secrets", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } }, { "node": { "id": "Qm9va05vZGU6Mw==", "title": "Harry Potter and the Prisoner of Azkaban", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } }, { "node": { "id": "Qm9va05vZGU6NA==", "title": "Harry Potter and the Goblet of Fire", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } } ] } } } |
EdgeとNodeが初めて出てきたので、戸惑うかもしれません。
イメージは大体こんな感じで、Relayのスキーマーはこのようになっています。
この実装は、Queryクラスの巨大化を防ぐことができそうですね。
BookNodeクラスは別ファイルに記述して、importすればよいと思います。
More Filter
filterを深堀します。
filterには、細かい指定を行うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class BookNode(DjangoObjectType): class Meta: model = Book filter_fields = { 'title': ['exact', 'icontains', 'istartswith'], 'publish': ['exact',], } interfaces = (relay.Node,) class Query(graphene.ObjectType): all_books = DjangoFilterConnectionField(BookNode) all_authors = graphene.List(AuthorType) book = relay.Node.Field(BookNode) # 修正 @login_required def resolve_all_authors(self, info, **kwargs): return Author.objects.all() # def resolve_book(self, info, **kwargs): # id = kwargs.get('id') # title = kwargs.get('title') # if id is not None: # return Book.objects.get(pk=id) # if title is not None: # return Book.objects.get(title=title) # return None |
このフィルターは、django-filterの機能で実現しています。
icontainsなどの機能は、Djangoのもともと備わっている機能と同じだと思います。 ブラウザをリロードして、実際にフィルターを試してみましょう。
完全一致(exact)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
query AllBook { allBooks (title: "Harry Potter and the Philosopher's Stone"){ edges { node { id title author { id firstname lastname } } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "data": { "allBooks": { "edges": [ { "node": { "id": "Qm9va05vZGU6MQ==", "title": "Harry Potter and the Philosopher's Stone", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } } ] } } } |
部分一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
query AllBook { allBooks (title_Icontains: "Philosopher's Stone"){ edges { node { id title author { id firstname lastname } } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "data": { "allBooks": { "edges": [ { "node": { "id": "Qm9va05vZGU6MQ==", "title": "Harry Potter and the Philosopher's Stone", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } } ] } } } |
最初の文字列一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
query AllBook { allBooks (title_Istartswith: "Harry Potter and the G"){ edges { node { id title author { id firstname lastname } } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "data": { "allBooks": { "edges": [ { "node": { "id": "Qm9va05vZGU6NA==", "title": "Harry Potter and the Goblet of Fire", "author": { "id": "1", "firstname": "Joanne", "lastname": "Rowling" } } } ] } } } |
やばすぎるほど簡単ですね。
Relay Mutation
Relay版のUpdate Mutationを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# app/api/schema.py import graphene import graphql_jwt from graphene import relay from graphene_django.types import DjangoObjectType from graphql_jwt.decorators import login_required from graphene_django.filter import DjangoFilterConnectionField from graphql_relay import from_global_id # new from .models import Author, Book … class BookUpdateMutationRelay( relay.ClientIDMutation ): class Input: title = graphene.String() id = graphene.ID(required=True) book = graphene.Field(BookType) @classmethod def mutate_and_get_payload(cls, root, info, id, title): # idがTW92aWVOb2RlOjMで取得してしまうので、数値に変換 # type, idの戻り値が変えるので、idのみ取得 book = Book.objects.get( pk=from_global_id(id)[1]) if title is not None: book.title = title book.save() return BookUpdateMutationRelay(book=book) … class Mutation: create_book = BookCreateMutation.Field() update_book = BookUpdateMutation.Field() update_book_relay = BookUpdateMutationRelay.Field() # new delete_book = BookDeleteMutation.Field() token_auth = graphql_jwt.ObtainJSONWebToken.Field() verify_token = graphql_jwt.Verify.Field() |
ブラウザをリロードして、以下のMutationを実行しましょう.
1 2 3 4 5 6 7 8 9 |
mutation MutateRelay { updateBookRelay(input: {id: "TW92aWVOb2RlOjE=", title: "test"}) { book { id title publish } } } |
1 2 3 4 5 6 7 8 9 10 11 |
{ "data": { "updateBookRelay": { "book": { "id": "1", "title": "test", "publish": 1997 } } } } |
ページネーション
ブラウザ上のDoc->allBooksを確認すると以下のパラメータが設定されています。
1 2 3 4 5 6 7 8 9 10 |
allBooks( before: String after: String first: Int last: Int title: String title_Icontains: String title_Istartswith: String publish: Int ): BookNodeConnection |
beforeやlastなどページネーションの機能を提供しています.
1 2 3 4 5 6 7 8 9 10 |
query AllBook { allBooks(last: 2) { edges{ node{ id title } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "data": { "allBooks": { "edges": [ { "node": { "id": "Qm9va05vZGU6Mw==", "title": "Harry Potter and the Prisoner of Azkaban" } }, { "node": { "id": "Qm9va05vZGU6NA==", "title": "Harry Potter and the Goblet of Fire" } } ] } } } |
スキーマーの検証
設定したスキーマーの検証を行うことができます。
settings.pyに次の設定をしてください。
1 2 3 4 5 6 7 8 |
GRAPHENE = { 'SCHEMA': 'app.schema.schema', 'SCHEMA_OUTPUT': 'data/myschema.json', # https://django-graphql-jwt.domake.io/en/latest/quickstart.html 'MIDDLEWARE': [ 'graphql_jwt.middleware.JSONWebTokenMiddleware', ], } |
app配下にdataフォルダを作成してください。
1 2 3 4 |
app L app L api L data |
次のコマンドを実行してください。
1 2 3 4 |
$ docker-compose run --rm app sh -c "python manage.py graphql_schema" Starting django-graphql_react_1 … done Starting django-graphql_db_1 … done Successfully dumped GraphQL schema to data/myschema.json |
上記のようにコマンド実行に成功したらdataフォルダ配下にmyschema.jsonが出力されていると思います。
Reactと結合
最後にReactと結合して終わりにしましょう。
settings.pyに以下の設定を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
INSTALLED_APPS = [ 'app.api', 'graphene_django', 'corsheaders', # new 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', # new 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] CORS_ORIGIN_ALLOW_ALL = True |
デフォルトだと、Djangoのセキュリティ機能によりReactからのリクエストを拒否してしまうので、上記の設定で回避します。
urls.pyにも同じくセキュリティ回避の設定をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# app/app/urls.py from django.contrib import admin from django.urls import path from graphene_django.views import GraphQLView from graphql_jwt.decorators import jwt_cookie from django.views.decorators.csrf import csrf_exempt urlpatterns = [ path('admin/', admin.site.urls), # path('graphql/', jwt_cookie(GraphQLView.as_view(graphiql=True))), path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))), ] |
続いて、Reactの設定です。frontend/src配下に次のファイルを作成してください。
1 |
touch frontend/src/books.js |
books.jsに次の設定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// frontend/src/books.js import React from 'react'; import gql from 'graphql-tag'; import { useQuery } from '@apollo/react-hooks'; const GET_MOVIES = gql` { allBooks { edges { node { id title } } } }`; function Books() { const { loading, error, data } = useQuery(GET_MOVIES); if (loading) return 'Loading...'; if (error) return `Error. ${error.message}`; const books = data.allBooks.edges; return ( <div> <h1>List of books - React</h1> { books.map(book => { return <h2 key={book.node.id}>{book.node.title}</h2> }) } </div> ) } export default Books; |
次にApp.jsに下記の設置を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import React from 'react'; import './App.css'; import Books from './books'; import { ApolloProvider } from '@apollo/react-hooks'; import ApolloClient from 'apollo-boost'; const client = new ApolloClient({ // the last of url needs "/" uri: 'http://127.0.0.1:8000/graphql/' }) function App() { return ( <ApolloProvider client={client}> <div className="App"> <Books /> </div> </ApolloProvider> ); } export default App; |
「localhost:3000」にアクセスしてみましょう。

Djangoのデータベースに登録されているデータを表示することができました。
おわりに
GraphQLは、かなり便利ですね。
現在、Djangoを使ったシステム開発に従事していますが、GraphQLサーバーの実装を提案しています。
まぁ、あまり新技術に乗り気ではない会社であるため、期待薄なのですが^^;
次はGo言語を使ったGraphQLも開発してみたいですね。
コメントを残す
コメントを投稿するにはログインしてください。