使用Django和Strawberry创建的GraphQL服务器

这篇文章是2022年Django日历活动的第十天文章。

首先

由于今年工作发生了变化,我对Django的接触机会减少了。虽然并不完全不再接触Django,我仍然会在自己的网站上小规模地使用它。我想和大家分享一下使用Django + Strawberry来构建GraphQL服务器的乐趣。

Python的GraphQL情况

提到Python的GraphQL,Graphene是非常有名的。我也尝试过使用Graphene-Django,感觉不错,但是由于开发活跃度不高,并且对Django 4.0的支持也比较晚(现在已经支持了),曾经有一段时间被认为是一个”Dead Project?”,甚至有人提出了相应的问题。

因此我找到了一个代替品,就是草莓。

草莓的特点

这个库是受到 Python 3.7 中引入的数据类(dataclasses)的启发而创建的。

草莓是一个适用于Python 3的新GraphQL库,受到dataclasses的启发。

可以用这种方式进行定义。很容易理解,对吧?

import typing
import strawberry

@strawberry.type
class Book:
    title: str
    author: str

@strawberry.type
class Query:
    books: typing.List[Book]

和许多其他库一样,它内置了GraphiQL用户界面,可以轻松进行操作验证。

与Django ORM进行整合

Strawberry 在 Python 的 Web 框架集成功能中也有相关支持,当然也支持 Django。

以下的代码是基于我个人私人网站的代码。

 

安装的东西

strawberry-graphql[debug-server]: Strawberryのコア(チュートリアルにあるもの)

strawberry-graphql-django: Djangoとの統合機能

与模型相匹配的类型

以下是对应于Django模型中的Inbox的InboxType的编写方式。您需要自己定义字段,但不必担心它们会被自动定义。

大致上可以使用自动的方法。它会根据Django的模型定义来处理得很好。

import strawberry_django
from strawberry import auto


@strawberry_django.type(Inbox)
class InboxType:
    id: auto

继承(Mixin)也是可能的,但在某些情况下需要定义 is_type_of(GenericModelType 和 DjangoModelType 是我自己创建的 Mixin)的一种方式是这样的。

@strawberry_django.type(Inbox)
class InboxType(GenericModelType, DjangoModelType):
    @classmethod
    def is_type_of(cls, root, info):
        return isinstance(root, (cls, Inbox))

在GraphQL中,有时候我们希望创建反向引用,而在Django ORM中,反向引用是默认支持的,所以可以很简单地编写。

@strawberry_django.type(Book)
class BookType(DjangoModelType, GenericModelType):
    @strawberry.field
    def references(self: Book) -> List[ReferenceType]:
        return self.reference_set.order_by("sort_order")

枚举

Enum在中文里面的翻译大致是这样的。很简单吧。

@strawberry.enum
class InboxState(Enum):
    ACTIVE = "active"
    ALL = "all"

查询

查询的格式大致如下(虽然类型提示的写法有些过时)。将其设定为Optional后,返回值从必填变为可选。

@strawberry.type
class InboxQuery:
    @strawberry_django.field
    def inboxes(self, state: InboxState = InboxState.ACTIVE) -> List[InboxType]:
        if state == InboxState.ALL:
            return Inbox.objects.all()
        elif state == InboxState.ACTIVE:
            return Inbox.objects.incomplete_only()
        else:  # pragma: no cover
            raise AssertionError

    @strawberry_django.field
    def inbox(self, number: int) -> Optional[InboxType]:
        return Inbox.objects.filter(pk=number).first()

突变

Mutation的使用方式如下。请注意GraphQL的Mutation具有返回值。

@strawberry.type
class InboxMutation:
    @strawberry.mutation
    def add_inbox(self, name: str) -> InboxType:
        inbox = Inbox.objects.create(name=name)

        return inbox

    @strawberry.mutation
    def done_inbox(self, inbox_id: int) -> InboxType:
        inbox: Inbox = get_object_or_404(Inbox, pk=inbox_id)
        inbox.status = InboxStatus.DONE
        inbox.save()

        return inbox

合并

就是这种感觉。

from strawberry.tools import merge_types

TaskQuery = merge_types(
    "TaskQuery",
    (
        ContextQuery,
        InboxQuery,
        LocationQuery,
        NextActionQuery,
        ProjectQuery,
        RepeatTaskQuery,
        SectionQuery,
        TaskInconsistenciesQuery,
    ),
)

模式

用这样的方式创建。Query和Mutation通过merge_types合并,Types指定类型的列表或元组。

import strawberry

schema = strawberry.Schema(query=Query, mutation=Mutation, types=Types)

观看

最后是”View”。使用”GraphQLView”这个类。这个类具有以下继承关系。

    • strawberry.django.views.GraphQLView

 

    • strawberry.django.views.BaseView

 

    django.views.generic.base.View

总评

开始时,要熟悉GraphQL的概念是有些困难的,但是一旦设置完成,维护起来就很容易了。特别是类型的编写方式非常简单,非常好用。

我也尝试了各种不同的GraphQL库(主要是TypeScript),但大致上有两种类型。

    • Resolverをスキーマから勝手に全部作ってくれるもの(Hasura, PostGraphileとか)

 

    Resolverを全部自分で書く必要があるもの

考虑到安全性和更名领域时的兼容性问题,前者不太理想,而后者则是我希望Mutation能变得容易,Query更轻松一些。

在Strawberry + Django中,字段可以明确指定,而类型则可以自动推断,这样非常方便。如果想将RDBMS直接转换为GraphQL结构但又想保留自由度,那就正好合适。

需要注意的地方是,该软件版本仍未达到1.0,并且有非常频繁的更新。截止到2022年12月5日,已经添加了488个标签。目前为止,还没有遇到与兼容性有关的问题。

广告
将在 10 秒后关闭
bannerAds