前篇我們提到了 REST API,這篇就來介紹這幾年很熱門的 GraphQL。GraphQL 是 Facebook 在 2015 年公開的技術,相較於 REST API,它提供了更強大、更靈活的功能,有以下特點:
Django 也可以使用 GraphQL,Django 需要安裝 graphene-django 來擴充。
poetry add graphene-django
首先在 settings 的 INSTALLED_APPS 加入 graphene_django,然後加入 GRAPHENE 設定。
# settings
INSTALLED_APPS = [
...
"django.contrib.staticfiles", # Required for GraphiQL
"graphene_django"
]
GRAPHENE = {
"SCHEMA": "news.schema.schema"
}
接著在 urls 加入 graphql 路徑
from django.urls import path
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
path("graphql", GraphQLView.as_view(graphiql=True)),
]
graphene-graphql 的使用方法跟 REST framwork 很相似,REST framework 是先定義 serializer ,再跟 APIView/ModelViewSet 搭配就可以組合出 REST API。graphene 則是先定義 query(供查詢) 跟 mutation (供寫入),再定義 schema ,就可以組合出一個 graphql 的 backend。
先定義 GraphQL type
import graphene
from graphene_django.types import DjangoObjectType, ObjectType
from .models import Article, Reporter
# Create a GraphQL type for the reporter model
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
# Create a GraphQL type for article model
class ArticleType(DjangoObjectType):
class Meta:
model = Article
再來定義 Query,表示透過 GraphQL 可以查詢到的資料,這裡我們定義了可以查單一的 reporter 跟 article,也可以查詢複數的 reporters 跟 articles。
# Create a Query type
class Query(ObjectType):
reporter = graphene.Field(ReporterType, id=graphene.Int())
article = graphene.Field(ArticleType, id=graphene.Int())
reporters = graphene.List(ReporterType)
articles= graphene.List(ArticleType)
def resolve_reporter(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Reporter.objects.get(pk=id)
return None
def resolve_article(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Article.objects.get(pk=id)
return None
def resolve_reporters(self, info, **kwargs):
return Reporter.objects.all()
def resolve_articles(self, info, **kwargs):
return Article.objects.all()
再來是定義 Mutation ,也就是新增跟更新資料,這邊會要先定義 Input,Input 就是參數,更新時,會依據 Input 來進行查詢。
# Mutations - Input object types
class ReporterInput(graphene.InputObjectType):
id = graphene.ID()
name = graphene.String()
class ArticleInput(graphene.InputObjectType):
id = graphene.ID()
title = graphene.String()
reporter = graphene.Field(ReporterInput)
再來才是 Mutation ,這是主要進行資料操作的類別
# Mutations
class CreateReporter(graphene.Mutation):
class Arguments:
input = ReporterInput(required=True)
ok = graphene.Boolean()
reporter = graphene.Field(ReporterType)
@staticmethod
def mutate(root, info, input=None):
ok = True
reporter_instance = Reporter(name=input.name)
reporter_instance.save()
return CreateReporter(ok=ok, reporter=reporter_instance)
class UpdateReporter(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = ReporterInput(required=True)
ok = graphene.Boolean()
reporter = graphene.Field(ReporterType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
reporter_instance = Reporter.objects.get(pk=id)
if reporter_instance:
ok = True
reporter_instance.name = input.name
reporter_instance.save()
return UpdateReporter(ok=ok, reporter=reporter_instance)
return UpdateReporter(ok=ok, reporter=None)
class CreateArticle(graphene.Mutation):
class Arguments:
input = ArticleInput(required=True)
ok = graphene.Boolean()
article = graphene.Field(ArticleType)
@staticmethod
def mutate(root, info, input=None):
ok = True
reporter_input = input.reporter
reporter = Reporter.objects.get(pk=reporter_input.id)
if reporter is None:
return CreateArticle(ok=False, article=None)
article_instance = Article(
title=input.title,
reporter=reporter
)
article_instance.save()
return CreateArticle(
ok=ok,
article=article_instance
)
class UpdateArticle(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = ArticleInput(required=True)
ok = graphene.Boolean()
article = graphene.Field(ArticleType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
article_instance = Article.objects.get(pk=id)
if article_instance:
ok = True
reporter_input = input.reporter
article_instance.title = input.title
if reporter_input is not None:
reporter = Reporter.objects.get(pk=reporter_input.id)
article_instance.reporter = reporter
article_instance.save()
return UpdateArticle(
ok=ok,
article=article_instance
)
return UpdateArticle(ok=ok, article=None)
上面我們分別定義了 CreateReporter、UpdateReporter、CreateArticle、UpdateArticle 用來建立、更新 Reporter 與 Article。
繼承 graphene.Mutation 以後,必須要填寫 Arguments 這個 Meta class,表明使用的 InputType 為何,以及回應的內容:ok 跟 article。然後是操作資料的部份 mutate() ,mutate 方法裡,會去依照輸入 (InputType) 去取得物件,然後進行資料操作。
最後,再定義 schema
class Mutation(graphene.ObjectType):
create_reporter = CreateReporter.Field()
update_reporter = UpdateReporter.Field()
create_article = CreateArticle.Field()
update_article = UpdateArticle.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
接著,就可以來測試啦
我們先查詢 reporters 看看
curl -H 'Content-Type: application/json' -d '{"query": "{ reporters {name} articles {id title reporter {name}}}"}' http://localhost:8000/graphql/
回傳的資料是
{"data":{"reporters":[],"articles":[]}}
這結果沒錯,因為目前還沒放資料進去。
試著建立 reporter 跟 article 試試看。
curl -H 'Content-Type: application/json' -d '{"query": "mutation createReporter { createReporter(input: { name: \"Tom Hanks\" }) { ok reporter { id name } } }"}' http://localhost:8000/graphql/
curl -H 'Content-Type: application/json' -d '{"query": "mutation createArticle { createArticle(input: { title: \"The Terminal\", reporter:{id: 1} }) { ok article { id title reporter {id name}} } }"}' http://localhost:8000/graphql/
然後再查詢一次
curl -H 'Content-Type: application/json' -d '{"query": "{ reporters {name} articles {id title reporter {name}}}"}' http://localhost:8000/graphql/
這時就會看到有資料啦
{"data":{"reporters":[{"name":"Tom Hanks"}],"articles":[{"id":"3","title":"The Terminal","reporter":{"name":"Tom Hanks"}}]}}
前面有提到,GraphQL 有即時更新的功能,也就是 subscription,但是目前 graphene 本身不支援 subscription,如果需要支援 graphql 的 subscription,那麼會需要額外的套件來輔助。我有找到兩個套件:
這兩個套件都不約而同的使用了 channels (主要是負責 websocket) 來提供 subscription 功能,但都還沒有試過。
測試範例專案網址:https://github.com/elleryq/ithome-iron-2020-django/tree/day-10
大致試驗了這個套件以後,對於 GraphQL 也有了一定的認識。GraphQL 可以帶來靈活的查詢,但是在後端的實作上,可以看到複雜度增加,對資料庫的操作也不是那麼有效率,所以實際在應用上,還是要看情況來考量,並不是一個萬靈丹。