iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
0

前篇我們提到了 REST API,這篇就來介紹這幾年很熱門的 GraphQL。GraphQL 是 Facebook 在 2015 年公開的技術,相較於 REST API,它提供了更強大、更靈活的功能,有以下特點:

  1. GraphQL 可以自我說明自己該怎麼使用,傳統的 REST API 通常需要額外去寫文件來說明 API 如何使用。
  2. GraphQL 的查詢跟更新是很彈性的,你可以指定所需階層、欄位等等,避免了傳遞大量資料所造成的頻寬浪費。
  3. 資料即時更新,透過 web socket ,當伺服器有資料更新時,客戶端會即時收到。

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 可以帶來靈活的查詢,但是在後端的實作上,可以看到複雜度增加,對資料庫的操作也不是那麼有效率,所以實際在應用上,還是要看情況來考量,並不是一個萬靈丹。

參考資料


上一篇
09. django-health-check
下一篇
11. django-debug-toolbar
系列文
加速你的 Django 網站開發 - Django 的好用套件30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言