Djnago-解耦教程-三-

139 阅读47分钟

Djnago 解耦教程(三)

原文:Decoupled Django

协议:CC BY-NC-SA 4.0

十一、Django 的 GraphQL 和 Ariadne

本章涵盖:

  • GraphQL 模式、操作和解析器

  • 阿里阿德涅

  • React 和 Django 与 GraphQL

在本章的第一部分,我们用一个 GraphQL API 扩充了第六章的计费应用。稍后,我们开始将 React/TypeScript 前端连接到 GraphQL 后端。

Note

本章假设您在 repo root decoupled-dj中,Python 虚拟环境是活动的,并且DJANGO_SETTINGS_MODULE被配置为decoupled_dj.settings.development

Django Ariadne 入门

第一章讲述了 GraphQL 的基础知识。

我们了解到,为了从 GraphQL API 获取数据,我们通过一个POST请求发送一个查询。为了改变数据库中的数据,我们发送了一个所谓的突变。现在是时候通过 Ariadne 将 GraphQL 引入我们的 Django 项目了,Ariadne 是一个用于 Python 的 GraphQL 库。Ariadne 使用模式优先的方法。在 schema-first 中,GraphQL API 由 GraphQL 模式定义语言以字符串或.graphql文件的形式形成。

安装 Ariadne

首先,我们在 Django 项目中安装 Ariadne:

pip install ariadne

安装完成后,我们更新requirements/base.txt以包含新的依赖项。接下来,我们在decoupled_dj/settings/base.py中启用 Ariadne,如清单 11-1 所示。

INSTALLED_APPS = [
      ...
      "ariadne.contrib.django",
]

Listing 11-1decoupled_dj/settings/base.py - Enabling Ariadne in INSTALLED_APPS

我们还需要确保TEMPLATES中的APP_DIRS被设置为True,如清单 11-2 所示。

TEMPLATES = [
      {
      ...
      "APP_DIRS": True,
      ...
]

Listing 11-2decoupled_dj/settings/base.py - Template Configuration

这就是我们要开始做的所有事情。启用 Ariadne 后,我们现在准备开始构建我们的 GraphQL 模式。

设计 GraphQL 模式

GraphQL 强制使用模式定义语言来定义模式,这是 GraphQL API 的主要构建块。

这与 REST 有很大的不同,在 REST 中,后端代码通常是第一位的,只有在后面我们才会为 API 生成文档。GraphQL 中的这个概念是颠倒的:我们首先创建模式,它既作为文档,又作为 GraphQL API 和它的消费者之间的契约。如果没有模式,我们就不能向 GraphQL API 发送查询或变更:我们会得到一个错误,因为模式驱动的正是消费者可以向服务询问的内容。模式中有什么?GraphQL 模式包含消费者可用的所有操作和模型实体的定义。在考虑我们的模式之前,让我们回顾一下计费应用中涉及的实体。我们有以下端点:

  • /billing/api/clients/

  • /billing/api/invoices/

此外,我们还有以下型号:

  • User

  • Invoice,用一个外键连接到User

  • ItemLine,用一个外键连接到Invoice

我们的 GraphQL 模式必须包含所有这些模型的形状(只要我们想在 API 中公开它们),加上每个允许的 GraphQL 操作的形状,以及它们的返回值。这在实践中意味着什么?为了向 GraphQL API 发送一个getClients查询,我们首先需要在模式中定义它的形状。以清单 11-3 中的查询为例。

query {
  getClients {
      name
  }
}

Listing 11-3Example of a Typical GraphQL Query

如果没有相应的模式,查询将会失败,并出现以下错误:

Cannot query field 'getClients' on type 'Query'

GraphQL 模式中所有可用的查询、变异和订阅都以操作命名。

有了这些知识,让我们定义我们的第一个模式。在billing/schema.graphql创建一个新文件。注意它有一个.graphql扩展名。大多数编辑器和 ide 为语言提供了补全和智能感知,因此将模式放在自己的文件中是有意义的。作为一种选择,我们也可以将模式直接写成代码中的三重引号字符串。在这个例子中,我们采用第一种方法。在该模式中,我们将为计费应用定义所有实体,外加从数据库获取所有客户端的第一个查询。我们从哪里得到物体的形状?由于我们的应用已经有了一个包含 DRF 的 REST API,我们可以查看一下billing/api/serializers.py,看看那里公开了哪些字段。毕竟,DRF 序列化器是 Django 模型和世界其他部分之间的公共接口,因此它是 GraphQL 模式的一种独特方式。此外,我们应该在billing/models.py中查看我们的模型是如何连接的,以便在 GraphQL 中表达相同的关系。清单 11-4 展示了我们第一个基于模型和 DRF 序列化器中定义的适当字段的模式。

enum InvoiceState {
   PAID
   UNPAID
   CANCELLED
}

type User {
   id: ID
   name: String
   email: String
}

type Invoice {
   user: User
   date: String
   dueDate: String
   state: InvoiceState
   items: [ItemLine]
}

type ItemLine {
   quantity: Int
   description: String
   price: Float
   taxed: Boolean
}

type Query {
   getClients: [User]
}

Listing 11-4billing/schema.graphql - First Iteration of the GraphQL Schema for the Billing App

从模式中我们可以立即识别出 GraphQL 标量类型:IDStringBooleanIntFloat。这些类型描述了对象的每个字段有什么类型的数据。我们还可以注意到一个enum类型,它代表了一个Invoice的不同状态。在我们的序列化器中,我们没有为Invoice公开state字段,但是现在是将它包含在 GraphQL 模式中的好时机。至于我们数据库中的模型实体,请注意UserInvoiceItemLine是带有字段的定制对象类型。另一件突出的事情是描述模型关系的方式。从上到下,我们可以看到:

  • Invoice为一个user字段为一个User

  • Invoice作为一个items字段添加到一个ItemLine列表中

  • getClients查询解析(返回)一个User列表

实际上,该模式是表达能力的最佳体现。同样值得注意的是,由于许多原因,这个模式还不完善。例如,InvoicedatedueDate表示为String。这不是 Django 所期望的。我们稍后将修复这些不一致之处。渴望看到 Django 中的 GraphQL API 是什么样子,在下一节中,我们将在 Ariadne 中加载模式。

在 Ariadne 中加载模式

我们离在 Django 建立第一个 GraphQL 端点只有几步之遥。

为此,我们需要加载模式并使其可执行。在billing/schema.py创建另一个文件,它应该包含清单 11-5 中的代码。

from ariadne import load_schema_from_path, gql, make_executable_schema
from ariadne import ObjectType
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

schema_file = load_schema_from_path(BASE_DIR / "schema.graphql")
type_defs = gql(schema_file)

query = ObjectType("Query")

"""
TODO: Write resolvers
"""

schema = make_executable_schema(type_defs, query)

Listing 11-5billing/schema.py - Loading the Schema in Ariadne

在这个代码片段中,我们从 Ariadne 导入必要的工具。特别是:

  • 从文件系统加载我们的模式文件

  • gql验证模式

  • ObjectType创建根类型(查询、变异和订阅)

  • make_executable_schema将模式连接到解析器,Python 函数对数据库执行查询

在第一个片段中,我们还没有任何解析器。我们将在下一节中添加一些。还需要注意的是,这段代码还没有对数据库做任何事情。在下一节中,我们将billing/schema.py连接到一个 Django URL。

连接 GraphQL 端点

在对 GraphQL API 进行查询之前,我们需要将我们的模式连接到 Django URL。

为此,我们在billing/urls.py中添加一个名为graphql/的 URL,如清单 11-6 所示。

from django.urls import path
from .views import Index
from .api.views import ClientList, InvoiceCreate
from ariadne.contrib.django.views import GraphQLView
from .schema import schema

app_name = "billing"

urlpatterns = [
   path("", Index.as_view(), name="index"),
   path("api/clients/", ClientList.as_view(), name="client-list"),
   path("api/invoices/", InvoiceCreate.as_view(), name="invoice-create"),
   path("graphql/", GraphQLView.as_view(schema=schema), name="graphql"),
]

Listing 11-6billing/urls.py - Enabling the GraphQL Endpoint

在 Django 的这个 URL 配置中,我们从 Ariadne 导入了GraphQLView,它的工作方式很像普通的 CBV。保存文件并用python manage.py runserver启动 Django 项目后,我们可以前往http://127.0.0.1:8000/billing/graphql/。这应该会打开 GraphQL Playground,一个用于探索 GraphQL API 的交互式工具。在操场上,我们可以发出查询、变更,或者只是探索模式和集成文档。图 11-1 显示了我们之前创建的模式,它出现在 GraphQL 平台上。

img/505838_1_En_11_Fig1_HTML.jpg

图 11-1

阿里阿德涅的 GraphQL 游乐场。它公开了模式和方便的文档

图 11-2 显示了我们第一个查询的自动生成文档。

img/505838_1_En_11_Fig2_HTML.jpg

图 11-2

我们的第一个查询出现在自动生成的文档中

为了向这个 GraphQL 端点发送查询,我们可以使用 playground 或者更实际的 JavaScript 客户机。快速测试,也可以用curl。打开另一个终端并启动以下命令:

curl -X POST --location "http://127.0.0.1:8000/billing/graphql/" \
      -H "Accept: application/json" \
      -H "Content-Type: application/json" \
      -d "{
      \"query\": \"query { getClients { name } }\"
      }"

作为响应,您应该得到以下结果:

{"data": {"getClients": null } }

这表明我们的 GraphQL API 正在工作,但是我们的查询没有返回任何东西。在下一节中,我们将讨论解决方案。

使用解析器

GraphQL 中的解析器是什么?最简单的形式是,解析器是一个负责查询数据库或任何其他来源的可调用的(一个方法或函数)。GraphQL 中的解析器是公共 API 和数据库之间的桥梁,这也是等式中最难的部分。GraphQL 只是一个规范,大多数实现都不包含数据库层。幸运的是,Django 有一个很好的 ORM 来减轻直接处理原始 SQL 查询的负担。让我们重温一下我们的模式,特别是清单 11-7 中显示的查询。

type Query {
      getClients: [User]
}

Listing 11-7Our First Query

这个查询有一个名为getClients的字段。为了让我们的getClients查询工作,我们需要将这个查询字段绑定到一个解析器。为此,我们回到billing/schema.py添加第一个解析器,如清单 11-8 所示。

from ariadne import load_schema_from_path, gql, make_executable_schema
from ariadne import ObjectType
from pathlib import Path
from users.models import User

BASE_DIR = Path(__file__).resolve().parent

schema_file = load_schema_from_path(BASE_DIR / "schema.graphql")
type_defs = gql(schema_file)

query = ObjectType("Query")

@query.field("getClients")
def resolve_clients(obj, info):
   return User.objects.all()

schema = make_executable_schema(type_defs, query)

Listing 11-8billing/schema.py - Adding a Resolver to the Mix

这个文件中最显著的变化是我们导入了User模型,并用@query.field()修饰了一个解析器函数。在这个例子中,解析器resolve_clients()得到两个参数:objinfoinfo包含关于被请求字段的细节,更重要的是,它携带一个附加了 HTTP 请求的上下文对象。这是同步运行的 Django 项目的一个实例WSGIRequestobj是从父解析器返回的任何值,因为解析器也可以嵌套。在我们的例子中,我们现在不使用这些参数。保存文件后,我们可以在每个用户的查询中包含任意多的字段。例如,我们可以在 GraphQL Playground 中发送清单 11-9 中的查询。

query {
  getClients {
      id
      name
      email
  }
}

Listing 11-9A GraphQL Query for Fetching All Clients

注意,在 Ariadne 的 Python 代码中,我们没有为idnameemail定义解析器。我们简单地为getClients查询定义了一个解析器。为每个字段手工定义一个解析器是不切实际的,幸运的是,GraphQL 涵盖了我们。Ariadne 可以用默认解析器的概念处理这些字段,这个概念来自于 Ariadne 构建的graphql-core。我们的查询现在将返回清单 11-10 中所示的响应。

{
    "data": {
        "getClients": [
            {
                "id": "1",
                "name": "Juliana",
                "email": "juliana@acme.io"
            },
            {
                "id": "2",
                "name": "John",
                "email": "john@zyx.dev"
            }
        ]
    }
}

Listing 11-10The Response from the GraphQL API

如果数据库中还没有用户,请从 shell 中创建一些:

python manage.py shell_plus

要创建两个用户,请运行以下查询(>>>是 shell 提示符):

>>> User.objects.create_user(username="jul81", name="Juliana", email="juliana@acme.io")
>>> User.objects.create_user(username="john89", name="John", email="john@zyx.dev")

Note

到目前为止,我们讨论了数据库,但是 GraphQL 几乎可以处理任何数据源。Gatsby 是这种能力的一个很好的例子:例如,Markdown 插件可以解析来自.md文件的数据。

祝贺您创建了第一个 GraphQL 端点!现在让我们了解一下 GraphQL 中的查询参数。

在 GraphQL 中使用查询参数

到目前为止,我们从数据库中获取了所有用户。

如果我们只想取一个呢?这就是查询参数发挥作用的地方。让我们想象下面的 GraphQL 查询:

query {
 getClient(id: 2) {
   name
   email
 }
}

这里我们有一个查询字段,使用起来很像一个函数。id是“函数”的自变量。相反,在主体中,我们声明了要为单个用户获取的字段。为了支持这个查询,模式必须知道它。这意味着我们需要:

  • 在模式中定义新的查询字段

  • 创建一个解析器来完成该字段

首先,在billing/schema.graphql中,我们将新字段添加到现有的Query对象中,如清单 11-11 所示(所有现有的对象类型必须保持不变)。

type Query {
   getClients: [User]
   getClient(id: ID!): User
}

Listing 11-11billing/schema.graphql - Adding a New Field to the Query

在新的getClient字段中,注意带有标量类型IDid参数,现在后面跟了一个感叹号。这个符号意味着参数是不可空的:由于显而易见的原因,我们不能使用空 ID 进行查询。

Note

不可空约束可应用于任何 GraphQL 对象字段或列表。

有了良好的模式,我们现在开始在billing/schema.py中定义新的解析器,如清单 11-12 所示(为了简洁,我们只显示了新的解析器)。

...
@query.field("getClient")
def resolve_client(obj, info, id):
   return User.objects.get(id=id)
...

Listing 11-12billing/schema.py - An Additional Resolver for Fulfilling the New Query

在这个新的解析器中,除了objinfo,我们还可以看到第三个名为id的参数。这个参数将由我们的 GraphQL 查询传递给解析器。通常,查询中定义的任意数量的参数都会传递给相应的解析器。我们现在可以在操场上发出清单 11-13 中所示的查询。

query {
 getClient(id: 2) {
   name
   email
 }
}

Listing 11-13Retrieving a Single User from the GraphQL Playground

服务器应该返回清单 11-14 中所示的响应。

{
 "data": {
   "getClient": {
     "name": "John",
     "email": "john@zyx.dev"
   }
 }
}

Listing 11-14The GraphQL Response for a Single User

在这次数据获取之旅之后,现在是时候做一些更有挑战性的事情了:添加带有突变的数据。

关于模式优先和代码优先的一句话

在我们的 GraphQL API 中添加了几个查询和解析器之后,我们已经可以发现一个模式了。

每当我们需要一个新字段时,我们就更新两个文件:

  • billing/schema.graphql

  • billing/schema.py

这个问题的解决方案是将文本模式放在代码中。考虑以下示例:

type_defs = gql(
   """
   type User {
       id: ID
       name: String
       email: String
   }
   """
)

我们没有从文件中加载模式,而是直接将它放在gql中。这是一种方便的方法。然而,使用代码优先的方法,从代码开始派生和生成模式的能力比处理字符串更加灵活。石墨烯和 Strawberry 正是遵循这条路径。

实施突变

GraphQL 中的突变是副作用,即意味着改变数据库状态的操作。

在第六章中,我们在 REST 后端工作,它接受来自前端的POST请求来创建新的发票。为了创建新的发票,我们的后端需要:

  • 要与发票关联的用户 ID

  • 发票日期

  • 发票到期日

  • 与发票关联的一个或多个项目行(数组)

为了在 GraphQL 中实现相同的逻辑,我们必须从突变的角度来考虑。突变具有以下特征:

  • 这不是一个查询

  • 这需要争论

我们已经看到了带有参数的 GraphQL 查询的外观。一个突变并没有太大的不同。考虑到创建新发票的需求,我们希望能够设计清单 11-15 中所示的变体。

mutation {
 invoiceCreate(invoice: {
   user: 1
   date: "2021-02-15"
   dueDate: "2021-02-15"
   state: PAID
   items: [{
     quantity: 1
     description: "Django backend"
     price: 6000.00
     taxed: false
   },
   {
     quantity: 1
     description: "React frontend"
     price: 8000.00
     taxed: false
   }]
 }) {
   user { id }
   date
   state
 }
}

Listing 11-15The Mutation Request for Creating a New Invoice

我们可以从 invoice 参数中看出,有效载荷不再是简单的标量,而是一个复杂的对象。这样的输入对象被称为输入类型,即强类型参数。我们可以在变异的签名中单独传递这些参数,但更好的是,我们可以利用 GraphQL 类型系统将单个对象的形状定义为一个参数。作为该查询的返回值,我们要求服务器使用以下数据进行响应:

  • 连接到新发票的用户 ID

  • 发票的日期和状态

在 GraphQL 中添加变异的过程与添加查询没有什么不同:

  • 首先,我们在模式中定义突变及其输入

  • 然后我们创建一个解析器来处理副作用

在 GraphQL 中,突变在Mutation类型下声明。为了创建我们的第一个变异,添加清单 11-16 到billing/schema.graphql中的代码。

type Mutation {
   invoiceCreate(invoice: InvoiceInput!): Invoice!
}

Listing 11-16billing/schema.graphql - The Mutation for Creating a New Invoice

invoiceCreate变异接受一个名为invoice的参数,类型为InvoiceInput,不可为空。作为交换,它返回一个不可空的Invoice。我们现在需要定义输入类型。它们应该是什么样子?首先,它们应该包含创建发票所需的所有字段。我们也不要忘记项目行是一个项目数组。在billing/schema.graphql中,我们创建了两种输入类型,如清单 11-17 所示。

input ItemLineInput {
   quantity: Int!
   description: String!
   price: Float!
   taxed: Boolean!
}

input InvoiceInput {
   user: ID!
   date: String!
   dueDate: String!
   state: InvoiceState
   items: [ItemLineInput!]!
}

type Mutation {
   invoiceCreate(invoice: InvoiceInput!): Invoice!
}

Listing 11-17billing/schema.graphql - Input Types for the Mutation

我们现在有:

  • InvoiceInput:用作变异参数类型的输入类型。它有一个ItemLineInputitems数组。

  • ItemLineInput:表示单项的输入类型,作为输入类型。

这些输入类型将反映在模式和文档中。有了模式,我们现在可以连接相应的解析器了。

Note

在对billing/schema.graphql进行更改之后,您应该重启 Django development server,否则更改将不会生效。

为突变添加解析器

有了突变的定义,我们现在可以添加相应的解析器了。

这个解析器将接触数据库以保存新的发票。为了实现这一点,我们需要在我们的模式中引入更多的代码,特别是:

  • MutationType:创建突变根类型

  • InvoiceItemLine:Django 模型

清单 11-18 显示了我们需要在billing/schema.py中进行的更改。

...
from ariadne import ObjectType, MutationType
...
from billing.models import Invoice, ItemLine

...

mutation = MutationType()

@mutation.field("invoiceCreate")
def resolve_invoice_create(obj, info, invoice):
   user_id = invoice.pop("user")
   items = invoice.pop("items")

   invoice = Invoice.objects.create(user_id=user_id, **invoice)
   for item in items:
       ItemLine.objects.create(invoice=invoice, **item)
   return invoice

schema = make_executable_schema(type_defs, query, mutation)

Listing 11-18billing/schema.py - Adding a Resolver to Fulfill the Mutation

这段代码中发生了很多事情。让我们来分解一下:

  • 我们使用MutationType()在 Ariadne 中创建新的突变根类型

  • 我们修饰突变解析器,使其映射到模式中定义的字段

  • 在解析器中,我们用 ORM 创建一个新的发票

  • 我们把突变和模式联系起来

Django 想要一个用户实例来创建新的发票,但是我们从 GraphQL 请求中得到的只是用户的 ID。这就是为什么我们从有效载荷中移除了user来将它作为user_id传递给Invoice.objects.create()。至于接下来的步骤,逻辑类似于我们在第五章中在串行化器中所做的。有了这些额外的代码,我们现在可以将清单 11-19 中所示的变异发送到 GraphQL。

mutation {
 invoiceCreate(invoice: {
   user: 1
   date: "2021-02-15"
   dueDate: "2021-02-15"
   state: PAID
   items: [{
     quantity: 1
     description: "Django backend"
     price: 6000.00
     taxed: false
   },
   {
     quantity: 1
     description: "React frontend"
     price: 8000.00
     taxed: false
   }]
 }) {
   user { id }
   date
   state
 }
}

Listing 11-19The Mutation Request for Creating a New Invoice

在 GraphQL Playground 中发出变异,您应该会看到以下错误:

Invoice() got an unexpected keyword argument 'dueDate'

这来自 Django 的 ORM 层。在我们的变异中,我们发出一个名为dueDate的字段,这是 camel case 中 GraphQL/JS 世界的惯例。然而,Django 期望due_date,正如它在模型中定义的那样。为了修复这种不匹配,我们可以使用阿里阿德涅的convert_kwargs_to_snake_case。打开billing/schema.py并应用清单 11-20 中所示的更改。

...
from ariadne.utils import convert_kwargs_to_snake_case
...

...
@mutation.field("invoiceCreate")
@convert_kwargs_to_snake_case
def resolve_invoice_create(obj, info, invoice):
...

Listing 11-20billing/schema.py - Converting from Camel Case to Snake Case

这里,我们用转换器工具来修饰我们的变异解析器。如果一切都在正确的位置,服务器现在应该返回以下响应:

{
 "data": {
   "invoiceCreate": {
     "user": {
       "id": "1",
       "email": "juliana@acme.io"
     },
     "date": "2021-02-15",
     "state": "PAID"
   }
 }
}

有了查询和变异,我们现在就可以将一个 React 前端连接到我们的 GraphQL 后端了。但是首先,我要说一下 GraphQL 客户端。

GraphQL 客户端简介

我们看到,就网络层而言,GraphQL 似乎不需要神秘的工具。

GraphQL 客户机和它的服务器之间的对话通过 HTTP 进行,带有POST请求。我们甚至可以用curl调用 GraphQL 服务。这意味着,在浏览器中,我们可以使用fetch、axios 甚至XMLHttpRequest对 GraphQL API 发出请求。在现实中,这对小应用来说可能很好,但迟早,在现实世界中,我们需要的不仅仅是fetch。具体来说,对于几乎每个数据提取层,我们都需要考虑某种缓存。由于 GraphQL 的工作方式,我们可以只请求数据的一个子集,但这并不排除节省往返服务器的需要。在接下来的部分中,我们将使用最流行的 JavaScript graph QL 客户端之一:Apollo 客户端。对于在前端使用 GraphQL 的开发人员来说,这个工具抽象出了性能优化的所有平凡细节。

构建 React 前端

在第六章,我们构建了一个用于创建发票的 Vue.js app。

该应用有一个<form>,它又包含一个<select>和一些用于插入发票详细信息的字段。对于这个 React 应用,我们构建了相同的结构,这次将每个子组件拆分到自己的文件中。在本章中,我们将使用查询部分。在第十二章中,我们看到了如何处理突变。首先,我们初始化一个 React 项目。我们移动到billing文件夹,然后启动create-react-app。要创建 React 项目,请运行以下命令:

npx create-react-app react_spa --template typescript

这将在billing/react_spa中创建项目。一旦项目就位,在新的终端移入文件夹:

cd react_spa

一旦 GraphQL 层就位,我们将从这个文件夹启动 React 应用。

Note

对于反应部分,我们在decoupled_dj/billing/react_spa中工作。必须从该路径开始,在适当的子文件夹中创建或更改每个建议的文件。

Apollo 客户端入门

首先,我们需要在项目中安装 Apollo 客户机。

为此,请运行以下命令:

npm i @apollo/client

一旦安装完成,打开src/App.tsx,清除所有的样板文件,并用清单 11-21 中所示的代码填充文件。

import {
 ApolloClient,
 InMemoryCache,
 gql,
} from "@apollo/client";

const client = new ApolloClient({
 uri: "http://127.0.0.1:8000/billing/graphql/",
 cache: new InMemoryCache(),
});

Listing 11-21src/App.tsx - Initializing Apollo Client

这里,我们通过提供 GraphQL 服务的 URL 来初始化客户端。ApolloClient构造函数至少接受这两个选项:

  • uri:graph QL 服务地址

  • cache:客户端的缓存策略

这里我们使用InMemoryCache,它是 Apollo 客户端中包含的默认缓存包。一旦我们有了一个客户端实例,要向服务器发送请求,我们可以使用:

  • client.query()发送查询

  • client.mutate()送突变

为了配合 React 使用,Apollo 还提供了一套方便的挂钩。在接下来的几节中,我们将创建三个 React 组件,并了解如何使用客户端方法,以及如何使用钩子。

创建选择组件

这个<select>是我们形态的一部分,会从外部接受道具。它应该为数据库中的每个用户呈现一个<option>元素。在src/Select.tsx中,我们创建了清单 11-22 中所示的组件。

import React from "react";

type Props = {
 id: string;
 name: string;
 options: Array<{
   id: string;
   email: string;
 }>;
};

const Select = ({ id, name, options }: Props) => {
 return (
   <select id={id} name={name} required={true}>
     <option value="">---</option>
     {options.map((option) => {
       return (
         <option value={option.id}>{option.email}</option>
       );
     })}
   </select>
 );
};

export default Select;

Listing 11-22src/Select.tsx - Select Component with TypeScript Definitions

该组件从外部接受一个选项列表以呈现给用户。在添加 GraphQL 之前,在 Vue.js 中,我们从 REST API 获取这些数据。相反,在这个应用中,我们让一个根组件处理数据获取,这次是从 GraphQL 服务,并将数据传递给<select>。现在让我们构建表单。

创建表单组件

表单组件相当简单,因为它接受一个处理submit事件的函数,以及一个或多个子组件。在src/Form.tsx中,我们创建了清单 11-23 中所示的组件。

import React from "react";

type Props = {
 children: React.ReactNode;
 handleSubmit(
   event: React.FormEvent<HTMLFormElement>
 ): void;
};

const Form = ({ children, handleSubmit }: Props) => {
 return <form onSubmit={handleSubmit}>{children}</form>;
};

export default Form;

Listing 11-23src/Form.tsx - Form Component with TypeScript Definitions

有了<form><select>之后,我们现在可以连接包含两者的根组件。

创建根组件并进行查询

每个 React 应用必须有一个根组件,负责渲染整个应用的外壳。为了简单起见,我们将在src/App.tsx中创建根组件。在我们的根组件中,我们需要:

  • 使用以下查询向 GraphQL 服务查询客户端列表

  • 处理提交事件,有一个突变

这里的想法是,当应用挂载时,我们使用useEffect()client.query()查询 GraphQL API。在 GraphQL Playground 中,我们使用以下查询来获取客户端列表:

query {
 getClients {
   id
   email
 }
}

在我们的 React 组件中,我们将使用相同的查询来获取客户端。这也是我们<select>的数据来源。组装查询时要记住的唯一一件事是,这是一个匿名查询,而在我们的 React 组件中,我们需要使用稍微不同的形式,作为命名查询。让我们在src/App.tsx中创建组件,如清单 11-24 所示。

import React, { useEffect, useState } from "react";
import {
 ApolloClient,
 InMemoryCache,
 gql,
} from "@apollo/client";
import Form from "./Form";
import Select from "./Select";

const client = new ApolloClient({
 uri: "http://127.0.0.1:8000/billing/graphql/",
 cache: new InMemoryCache(),
});

const App = () => {
 const [options, setOptions] = useState([
   { id: "", email: "" },
 ]);

 const handleSubmit = (
   event: React.FormEvent<HTMLFormElement>
 ) => {
   event.preventDefault();
   // client.mutate()
 };

 const GET_CLIENTS = gql`
   query getClients {
     getClients {
       id
       email
     }
   }
 `;

 useEffect(() => {
   client
     .query({
       query: GET_CLIENTS,
     })
     .then((queryResult) => {
       setOptions(queryResult.data.getClients);
     })
     .catch((error) => {
       console.log(error);
     });
 }, []);

 return (
   <Form handleSubmit={handleSubmit}>
     <Select id="user" name="user" options={options} />
   </Form>
 );
};

export default App;

Listing 11-24src/App.tsx - React Component for Fetching Data

让我们详细解释一下这段代码的内容:

  • 我们为阿波罗客户端保留逻辑

  • 我们使用useState()来初始化组件的状态

  • 该状态包含了作为道具传递的<select>的选项列表

  • 我们定义了一个处理submit事件的最小方法

  • 我们使用useEffect()从 GraphQL API 获取数据

  • 我们将FormSelect呈现给用户

阿波罗部分也值得一读。首先,查询如清单 11-25 所示。

...
 const GET_CLIENTS = gql`
   query getClients {
     getClients {
       id
       email
     }
   }
 `;
...

Listing 11-25Building the Query

这里我们使用gql将查询包装在一个模板文字标签中。这将生成一个 GraphQL 抽象语法树,供实际的 GraphQL 客户端使用。接下来,让我们检查发送查询的逻辑:

...
   client
     .query({
       query: GET_CLIENTS,
     })
     .then((queryResult) => {
       setOptions(queryResult.data.getClients);
     })
     .catch((error) => {
       console.log(error);
     });
...

在这个逻辑中,我们通过为一个对象提供一个query属性来调用client.query(),这个属性被分配给前面的查询。client.query()回报诺言。这意味着我们可以使用then()来消费结果,使用catch()来处理错误。在then()中,我们访问查询结果,并使用来自组件状态的setOptions来保存结果。在data.getClients上可以访问查询结果,这恰好是我们查询的名称。这看起来有点冗长。事实上,Apollo 提供了一个useQuery()钩子来减少样板文件,我们马上就会看到。保存好所有的东西后,为了进行测试,我们应该运行 Django,像往常一样在终端中运行decoupled-dj:

python manage.py runserver

在另一个终端中,从/billing/react_spa我们可以运行 React 应用:

npm start

这将在http://localhost:3000/启动 React。在用户界面中,我们应该能够看到一个select元素,其中每个选项呈现了每个客户的 ID 和电子邮件,如图 11-3 所示。

img/505838_1_En_11_Fig3_HTML.jpg

图 11-3

选择组件从 GraphQL API 接收数据

这可能看起来不像是一个巨大的成就,与用 axios 或fetch获取数据没有太大区别。在下一节中,我们将看到如何用 Apollo 钩子来清理 React 的逻辑。

使用阿波罗挂钩进行反应

Apollo 客户端并不劝阻使用client.query()

然而,在 React 应用中,开发人员可能希望使用 Apollo 钩子来保持代码库的一致性,就像我们习惯于在组件中使用useState()useEffect()一样。Apollo Client 包含一组钩子,使得在 React 中使用 GraphQL 变得轻而易举。要进行查询,我们可以使用useQuery()钩子来代替client.query()。然而,在我们的应用中,这需要一点重新安排。首先,我们需要用ApolloProvider包装整个应用。对于那些熟悉 Redux 或 React 上下文 API 的人来说,这是 Redux Provider或 React 上下文对等物所公开的相同概念。在我们的应用中,我们首先需要在src/index.tsx中移动 Apollo 客户端实例化。在同一个文件中,我们还用提供者包装了整个应用。清单 11-26 展示了我们需要做出的改变。

import React from "react";
import ReactDOM from "react-dom";
import {
 ApolloClient,
 InMemoryCache,
 ApolloProvider,
} from "@apollo/client";
import App from "./App";

const client = new ApolloClient({
 uri: "http://127.0.0.1:8000/billing/graphql/",
 cache: new InMemoryCache(),
});

ReactDOM.render(
 <React.StrictMode>
   <ApolloProvider client={client}>
     <App />
   </ApolloProvider>
 </React.StrictMode>,
 document.getElementById("root")
);

Listing 11-26src/index.tsx - The App Shell

现在,在src/App.tsx中,我们只从 Apollo 客户端导入了gqluseQuery,另外,我们通过将查询移出组件来对其进行一些安排。在组件体中,我们使用useQuery(),如清单 11-27 所示。

import React from "react";
import { gql, useQuery } from "@apollo/client";
import Form from "./Form";
import Select from "./Select";

const GET_CLIENTS = gql`
 query getClients {
   getClients {
     id
     email
   }
 }
`;

const App = () => {
 const { loading, data } = useQuery(GET_CLIENTS);

 const handleSubmit = (
   event: React.FormEvent<HTMLFormElement>
 ) => {
   event.preventDefault();
   // client.mutate()
 };

 return loading ? (
   <p>Loading ...</p>
 ) : (
   <Form handleSubmit={handleSubmit}>
     <Select
       id="user"
       name="user"
       options={data.getClients}
     />
   </Form>
 );
};

export default App;

Listing 11-27src/App.tsx - GraphQL Query with Apollo Hook

FormSelect都可以保持不变。我们还可以注意到,useQuery()将我们的查询作为一个参数,并免费返回一个loading布尔值(便于条件渲染)和一个包含查询结果的data对象。这比client.query()干净多了。如果我们再次运行这个项目,一切应该仍然像预期的那样工作,在 UI 中呈现一个<select>。通过这一改变,我们现在可以充分利用钩子的声明式风格,与 GraphQL 配合使用。

Note

这是提交到目前为止所做的更改并将工作推送到 Git repo 的好时机。你可以在 https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_11_graphql_ariadne 找到本章的源代码。

摘要

本章用 GraphQL API 扩充了第六章的计费应用。您还看到了如何将 React 连接到 GraphQL 后端。在此过程中,您了解到:

  • GraphQL 构建模块

  • 使用 Ariadne 向 Django 项目添加 GraphQL

  • 将 React 连接到 GraphQL 后端

在下一章中,我们将继续在 Django 中探索 GraphQL,使用 Strawberry,并且我们还在混合中添加了一些变化。

额外资源

十二、Django 的 GraphQL 和 Strawberry

本章涵盖:

  • 带 Strawberry 的代码优先 GraphQL

  • 异步 Django 和 GraphQL

  • Apollo 客户端的变化

在前一章中,我们介绍了使用 Ariadne 的 GraphQL APIs 的模式优先的概念。

我们研究了查询和 Apollo 客户端。在这一章中,我们切换到代码优先的方法,用 Strawberry 构建我们的 GraphQL API。在这个过程中,我们在前端添加了变异,并学习了如何在 Django 中使用异步代码。

Note

本章的其余部分假设您在 repo root decoupled-dj中,Python 虚拟环境是活动的,并且DJANGO_SETTINGS_MODULE被配置为decoupled_dj.settings.development

Django Strawberry 入门

一开始,GraphQL 主要针对 JavaScript。

GraphQL 服务器的大多数早期实现都是为 Node.js 编写的,这并不是巧合。随着时间的推移,大多数编程社区对这种新的数据查询范式产生了兴趣,现在我们已经有了大多数语言的 GraphQL 实现。在 Python 领域,我们探索了 Ariadne,我们提到了石墨烯。Strawberry 和这些库有什么不同?首先,Strawberry 大量使用 Python 数据类。Python 中的 Dataclasses 是用属性和可选逻辑声明简洁类的简单方法。以下示例显示了一个 Python 数据类:

class User:
   name: str
   email: str

在这个例子中,我们声明了一个没有行为的 Python 类,但是有两个属性,nameemail。这些属性也是强类型的;也就是说,他们能够强制执行他们可以持有的类型,在本例中是字符串。Strawberry 大量使用 Python 类型提示。类型提示是一个可选的 Python 特性,可以提高我们代码的健壮性。Python 很像 JavaScript,是一种不强制静态类型的动态语言。通过类型提示,我们可以在 Python 代码中添加一个类型层,在生产中发布代码之前,可以使用一个名为 MyPy 的工具对其进行检查。这可以捕捉可能进入运行时环境的讨厌的 bug。此外,静态类型检查改善了开发人员的体验。在 Strawberry 中,我们将使用 dataclasses 来定义我们的 GraphQL 类型和类型提示。该练习了!

安装 Strawberry

首先,我们在 Django 项目中安装 Strawberry:

pip install strawberry-graphql

安装完成后,我们更新requirements/base.txt以包含新的依赖项。接下来,我们在decoupled_dj/settings/base.py中启用 Strawberry,如清单 12-1 所示。

INSTALLED_APPS = [
      ...
      "strawberry.django",
]

Listing 12-1decoupled_dj/settings/base.py - Enabling Strawberry in INSTALLED_APPS

启用 Strawberry 后,我们可以从模式优先到代码优先来重构模式。

在 Strawberry 中设计 GraphQL 模式

在前一章中,我们在一个.graphql文件中创建了一个 GraphQL 模式。

让我们回顾一下目前我们所掌握的情况。清单 12-2 显示了我们在第十一章中组装的 GraphQL 模式。

enum InvoiceState {
   PAID
   UNPAID
   CANCELLED
}

type User {
   id: ID
   name: String
   email: String
}

type Invoice {
   user: User
   date: String
   dueDate: String
   state: InvoiceState
   items: [ItemLine]
}

type ItemLine {
   quantity: Int
   description: String
   price: Float
   taxed: Boolean
}

type Query {
   getClients: [User]
   getClient(id: ID!): User
}

input ItemLineInput {
   quantity: Int!
   description: String!
   price: Float!
   taxed: Boolean!
}

input InvoiceInput {
   user: ID!
   date: String!
   dueDate: String!
   state: InvoiceState
   items: [ItemLineInput!]!
}

type Mutation {
   invoiceCreate(invoice: InvoiceInput!): Invoice!
}

Listing 12-2billing/schema.graphql - The Original GraphQL Schema

在这个模式中,我们使用了该语言提供的大多数 GraphQL 标量类型,加上我们的自定义类型和输入类型定义。我们还创建了两个查询和一个变异。为了理解 Strawberry 所提供的功能,让我们将模式中的每个元素从纯文本模式移植到 Python 代码中。

Strawberry 的类型和枚举

首先,我们从 GraphQL 模式的基本类型开始。

我们需要声明UserInvoiceItemLine。要创建模式,打开billing/schema.py,清除我们在第十一章中创建的所有代码,并导入清单 12-3 中显示的模块。

import strawberry
import datetime
import decimal

from typing import List

Listing 12-3billing/schema.py - Initial Imports

typing是主要的 Python 模块,从中我们可以仔细阅读最常见的类型声明。接下来是strawberry本身。我们还需要decimal模块和精华datetime。接下来,我们准备创建我们的第一个类型。清单 12-4 显示了 Strawberry 中的三种 GraphQL 类型。

import strawberry
import datetime
import decimal

from typing import List

@strawberry.type
class User:
   id: strawberry.ID
   name: str
   email: str

@strawberry.type
class Invoice:
   user: User
   date: datetime.date
   due_date: datetime.date
   state: InvoiceState
   items: List["ItemLine"]

@strawberry.type
class ItemLine:
   quantity: int
   description: str
   price: decimal.Decimal
   taxed: bool

Listing 12-4billing/schema.py - First Types in Strawberry

对于刚接触 Python 类型的人来说,这里有很多东西需要解释一下。谢天谢地,Python 的表达能力足够强,不会让事情变得过于复杂。让我们从头开始。

为了在 Strawberry 中声明一个新的 GraphQL 类型,我们使用了@strawberry.type decorator,它位于我们的 dataclasses 之上。接下来,将每个类型声明为一个 dataclass,每个类型包含一组属性。在第 1 和 11 章中,我们看到了 GraphQL 标量类型。在 Strawberry 中,除了strawberry.ID,没有什么特别的东西可以描述这些标量。正如您在清单 12-4 中看到的,大多数标量类型被表示为 Python 原语:strintbool。唯一的例外是datedue_date的类型,我们在最初的 GraphQL 模式中将其声明为字符串。因为 Strawberry 中的类型是数据类,而数据类“只是”Python 代码,而不是日期字符串,所以我们现在可以使用datetime.date对象。这是我们在第十一章中未解决的问题之一,现在已经解决了。

Note

你可能想知道这里的due_date和前一章的dueDate有什么关系。在最初的 GraphQL 模式中,我们在 camel 案例中使用了dueDate。在到达 Django ORM 之前,Ariadne 将该语法转换为 snake case。现在我们在 GraphQL 模式中再次使用 snake case。为什么呢?作为 Python 代码,惯例是对较长的变量和函数名使用 snake case。但是这一次,转换以相反的方式发生:在 GraphQL 文档模式中,Strawberry 将字段显示为 camel case!

接下来,请注意如何通过将 dataclass 属性与相应的实体相关联来描述关系,比如在Invoice中将User dataclass 分配给user。还要注意来自 Python typings 的List类型将ItemLine关联到items。之前,我们使用 GraphQL 中的Float标量来表示每个ItemLine的价格。在 Python 中,我们可以使用更合适的decimal.Decimal。即使从这样一个简单的清单中,我们也可以推断出用 Python 代码编写 GraphQL 模式的 Strawberry 方法带来了很多好处,包括类型安全性、灵活性和对标量类型的更好处理。

在最初的 GraphQL 模式中,我们有一个与Invoice相关联的enum类型,它指定发票是已支付、未支付还是已取消。在新的模式中,我们已经有了Invoice,所以这是一个添加枚举的问题。在 Strawberry 中,我们可以使用普通的 Python 枚举来声明相应的 GraphQL 类型。在模式文件中,添加枚举,如清单 12-5 (这应该在User之前)。

...
from enum import Enum

@strawberry.enum
class InvoiceState(Enum):
   PAID = "PAID"
   UNPAID = "UNPAID"
   CANCELLED = "CANCELLED"
...

Listing 12-5billing/schema.py - Enum Type in Strawberry

这与我们在 Django 的Invoice模型的选择非常相似。只要有一点创造力,就可以在 Django 模型中重用这个 Strawberry 枚举(或者反过来)。有了 enum,我们几乎可以开始测试了。让我们在接下来的部分中添加解析器和查询。

使用解析器(再次)

我们已经知道 GraphQL 模式需要解析器来返回数据。

让我们添加两个解析器到我们的模式中,这两个解析器几乎是直接从第十一章复制过来的(参见清单 12-6 )。

...
from users.models import User as UserModel
...

def resolve_clients():
   return UserModel.objects.all()

def resolve_client(id: strawberry.ID):
   return UserModel.objects.get(id=id)

Listing 12-6billing/schema.py - Adding Resolvers to the Schema

为了避免与这里的User GraphQL 类型冲突,我们将用户模型导入为UserModel。接下来,我们声明解析器来完成最初的 GraphQL 查询,即getClientgetClients。请注意我们如何将一个id作为参数传递给第二个解析器,以便像在上一章中那样通过 ID 获取单个用户。有了这些解析器,我们可以添加一个Query类型,最后在下一节中连接 GraphQL 端点。

Strawberry 中的查询和连接 GraphQL 端点

有了基本类型和解析器,我们可以为我们的 API 创建一个代码优先的Query类型。

将清单 12-7 中所示的代码添加到模式文件中。

@strawberry.type
class Query:
   get_clients: List[User] = strawberry.field(resolver=resolve_clients)
   get_client: User = strawberry.field(resolver=resolve_client)
schema = strawberry.Schema(query=Query)

Listing 12-7billing/schema.py - Adding a Root Query Type

这里我们告诉 Strawberry graph QL 有一个带有两个字段的Query类型。让我们详细看看这些字段:

  • get_clients返回一列User并连接到名为resolve_clients的解析器

  • get_client返回单个User并连接到名为resolve_client的解析器

两个分解器都用strawberry.field()包裹。注意,在 GraphQL 文档中,这两个查询都将被转换为 camel case,即使它们在我们的代码中被声明为 snake case。在最后一行中,我们将模式加载到 Strawberry 中,因此它被提取出来并提供给用户。值得注意的是,Strawberry 中的解析器不必与Query数据类本身断开连接。事实上,我们可以在Query中将它们声明为方法。我们将这两个解析器留在数据类之外,但是我们稍后将看到作为Mutation数据类的方法的变异。

有了这个逻辑,我们可以在billing/urls.py中将 GraphQL 层连接到 Django URL 系统。从 Ariadne 中删除 GraphQL 视图。这一次,我们使用来自 Strawberry 的异步 GraphQL 视图,而不是常规视图,如清单 12-8 所示。

...
from strawberry.django.views import AsyncGraphQLView
...

app_name = "billing"

urlpatterns = [
   ...
   path("graphql/",
        AsyncGraphQLView.as_view(schema=schema),
        name="graphql"
        ),
]

Listing 12-8billing/urls.py - Wiring Up the GraphQL Endpoint

通过异步运行 GraphQL API,我们有了一个新的可能性的世界,但也有许多新的东西要考虑,我们马上就会看到。我们将在接下来的章节中探索一个例子。记住,要异步运行 Django,我们需要一个像 Uvicorn 这样支持 ASG 的服务器。我们在第五章安装了这个包,但是回顾一下,你可以用下面的命令安装 Uvicorn:

pip install uvicorn

接下来,导出DJANGO_SETTINGS_MODULE环境变量,如果您还没有这样做的话:

export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.development

最后,使用以下命令运行服务器:

uvicorn decoupled_dj.asgi:application --reload

标志确保 Uvicorn 在文件改变时重新加载。如果一切顺利,您应该会看到以下输出:

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

在接下来的部分中,我们将在 Uvicorn 下运行 Django。现在我们可以前往http://127.0.0.1:8000/billing/graphql/。这将打开 GraphQL,一个探索 GraphQL API 的平台。在操场上,我们可以发出查询和突变,探索模式和集成文档,就像我们对 Ariadne 所做的那样。现在您已经有了大致的了解,您可以用输入类型和突变来完成模式。

Strawberry 的输入类型和突变

我们看到 GraphQL 中的输入类型基本上是突变的参数。

为了在 Strawberry 中定义输入类型,我们仍然创建一个 dataclass,但是这一次我们在它上面使用了@strawberry.input装饰器。让我们为ItemLineInputInvoiceInput创建两个输入类型(这段代码可以放在Query类型之后);见清单 12-9 。

...
@strawberry.input
class ItemLineInput:
   quantity: int
   description: str
   price: decimal.Decimal
   taxed: bool

@strawberry.input
class InvoiceInput:
   user: strawberry.ID
   date: datetime.date
   due_date: datetime.date
   state: InvoiceState
   items: List[ItemLineInput]

Listing 12-9billing/schema.py - Adding Input Types to the Schema

这里我们有数据类,它们具有输入类型的适当属性。作为糖衣,...哎呀,Strawberry,在蛋糕上,让我们也添加一个突变(见清单 12-10 )。

...
import dataclasses
...
from billing.models import Invoice as InvoiceModel
from billing.models import ItemLine as ItemLineModel
...
@strawberry.type
class Mutation:
   @strawberry.mutation
   def create_invoice(self, invoice: InvoiceInput) -> Invoice:
       _invoice = dataclasses.asdict(invoice)
       user_id = _invoice.pop("user")
       items = _invoice.pop("items")
       state = _invoice.pop("state")

       new_invoice = InvoiceModel.objects.create(
           user_id=user_id, state=state.value, **_invoice
       )
       for item in items:
           ItemLineModel.objects.create(invoice=_invoice, **item)
       return new_invoice

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

Listing 12-10billing/schema.py - Adding a Mutation to the Schema

这段代码需要一点解释。首先,我们再次导入 Django 模型,这一次通过别名化它们来避免与数据类冲突。接下来,我们定义一个Mutation和其中的一个方法。名为create_invoice()的方法将InvoiceInput输入类型作为参数,并用@strawberry.mutation修饰。在方法内部,我们将 dataclass 输入类型转换为字典。这很重要,因为突变参数是一个数据类,而不是一个字典。这样,我们可以弹出我们需要的键,就像我们对 Ariadne 所做的那样。在突变中,我们还弹出了state,后来作为state.value传递到InvoiceModel.objects.create()。在撰写本文时,Strawberry 没有自动将枚举键转换为字符串,所以我们需要做一些数据钻取。最后,注意这个变异的返回值的类型注释,Invoice。在文件的最后,我们还将突变数据类加载到模式中。

变异和输入类型现在完成了我们的 GraphQL 模式。在这个阶段,我们可以使用 GraphiQL Playground 发送一个invoiceCreate变异,但是我们将在 React 前端实现变异,而不是手动尝试。但是首先,让我们看看用 Django 异步运行 Strawberry 的含义。

与 Django ORM 异步工作

在设置好一切之后,您可能已经注意到,在 GraphiQL 中发送一个简单的查询,一切都会变得一团糟。

即使发出一个简单的查询,Django 也会响应以下错误:

You cannot call this from an async context - use a thread or sync_to_async.

这个错误有点神秘,但是它来自 ORM 层。在撰写本文时,Django 的 ORM 还不支持异步。这意味着我们不能在异步运行 Django 时简单地启动 ORM 查询。为了解决这个问题,我们需要用 ASGI 的一个名为sync_to_async的异步适配器来包装 ORM 交互。为了便于理解,我们首先将实际的 ORM 查询转移到单独的函数中。然后,我们用sync_to_async包装这些函数。清单 12-11 显示了所需的变更。

...
from asgiref.sync import sync_to_async
...

def _get_all_clients():
   return list(UserModel.objects.all())

async def resolve_clients():
   return await sync_to_async(_get_all_clients)()

def _get_client(id):
   return UserModel.objects.get(id=id)

async def resolve_client(id: strawberry.ID):
   return await sync_to_async(_get_client)(id)

Listing 12-11billing/schema.py - Converting ORM Queries to Work Asynchronously

让我们看看这是怎么回事。首先,我们将 ORM 逻辑转移到两个常规函数中。在第一个函数_get_all_clients()中,我们用.all()从数据库中获取所有客户端。我们还强制 Django 通过将查询集转换成一个带有list()的列表来计算查询集。有必要在异步上下文中评估查询,因为 Django querysets 在默认情况下是懒惰的。在第二个函数_get_client()中,我们简单地从数据库中获取一个用户。然后这两个函数在两个异步函数中被调用,包装在sync_to_async()中。这种机制将使 ORM 代码在 ASGI 下工作。

解析器不是唯一需要异步包装器的部分。虽然在这个阶段,我们不希望任何人猛烈攻击我们的 GraphQL 变种,但是保存新发票的 ORM 代码也需要包装。同样,我们可以取出 ORM 相关的代码来分离函数,然后用sync_to_async包装这些函数,如清单 12-12 所示。

...
from asgiref.sync import sync_to_async
...
def _create_invoice(user_id, state, invoice):
   return InvoiceModel.objects.create(user_id=user_id, state=state.value, **invoice)

def _create_itemlines(invoice, item):
   ItemLineModel.objects.create(invoice=invoice, **item)

@strawberry.type
class Mutation:
   @strawberry.mutation
   async def create_invoice(self, invoice: InvoiceInput) -> Invoice:
       _invoice = dataclasses.asdict(invoice)
       user_id = _invoice.pop("user")
       items = _invoice.pop("items")
       state = _invoice.pop("state")

       new_invoice = await sync_to_async(_create_invoice)(user_id, state, _invoice)
       for item in items:
           await sync_to_async(_create_itemlines)(new_invoice, item)
       return new_invoice

Listing 12-12billing/schema.py - Converting ORM Queries to Work Asynchronously

这看起来像是做了很多 Django 提供的现成的代码,即 SQL 查询,但是这是为了异步运行 Django 所付出的代价。在未来,我们希望对 ORM 层有更好的异步支持。现在,有了这些改变,我们可以异步地并行运行 Strawberry 和 Django 了。我们现在可以移到前端,用 Apollo 客户机实现突变。

又在前端工作了

在第十一章,我们开始开发一个 React/TypeScript 前端,它充当我们的 GraphQL API 的客户端。

到目前为止,我们在前端为一个<select>组件实现了一个简单的查询。首先,我们使用 Apollo client.query(),这是一种低级的查询方法。然后,我们重构使用了useQuery()钩子。在接下来的章节中,我们用 Apollo 客户端和useMutation()处理前端的突变。

Note

对于反应部分,我们在decoupled_dj/billing/react_spa中工作。必须从该路径开始,在适当的子文件夹中创建或更改每个建议的文件。

使用变异创建发票

我们离开前一章,使用清单 12-13 中显示的App组件。

import React from "react";
import { gql, useQuery } from "@apollo/client";
import Form from "./Form";
import Select from "./Select";

const GET_CLIENTS = gql`
 query getClients {
   getClients {
     id
     email
   }
 }
`;

const App = () => {
 const { loading, data } = useQuery(GET_CLIENTS);

 const handleSubmit = (
   event: React.FormEvent<HTMLFormElement>
 ) => {
   event.preventDefault();
   // client.mutate()
 };

 return loading ? (
   <p>Loading ...</p>
 ) : (
   <Form handleSubmit={handleSubmit}>
     <Select
       id="user"
       name="user"
       options={data.getClients}
     />
   </Form>
 );
};

export default App;

Listing 12-13src/App.tsx - GraphQL Query with Apollo

这个组件使用一个查询来填充装载在 DOM 中的<select>。现在是实施突变的时候了。到目前为止,在 Ariadne 中,我们通过在 GraphQL Playground 中提供突变负载来发送突变。这一次,前端发生了一些变化:我们需要从 Apollo 客户端使用useMutation()。首先,我们导入新的钩子,如清单 12-14 所示。

...
import { gql, useQuery, useMutation } from "@apollo/client";
...

Listing 12-14src/App.tsx - Importing useMutation

接下来,就在GET_CLIENTS查询之后,我们声明一个名为CREATE_INVOICE的变异,如清单 12-15 所示。

...
const CREATE_INVOICE = gql`
 mutation createInvoice($invoice: InvoiceInput!) {
   createInvoice(invoice: $invoice) {
     date
     state
   }
 }
`;
...

Listing 12-15src/App.tsx - Declaring a Mutation

这种变异看起来有点像函数,因为它接受一个参数并向调用者返回一些数据。但是这种情况下的参数是输入类型。现在,在App中,我们使用新的钩子。useMutation()的用法让人想起 React 中的useState():我们可以从钩子中数组化析构一个函数。清单 12-16 展示了我们组件中的变异钩子。

...
const App = () => {
 const { loading, data } = useQuery(GET_CLIENTS);
 const [createInvoice] = useMutation(CREATE_INVOICE);
...

Listing 12-16src/App.tsx - Using the useMutation Hook

另外,我们还可以用两个属性来析构一个对象:errorloading。与查询一样,这些将在出现错误时提供信息,并在变异期间提供加载状态以有条件地呈现 UI。为了避免与查询中的loading冲突,我们给变异加载器分配了一个新名称。清单 12-17 显示了这些变化。

...
const App = () => {
  const { loading, data } = useQuery(GET_CLIENTS);
  const [
      createInvoice,
      { error, loading: mutationLoading },
  ] = useMutation(CREATE_INVOICE);
...

Listing 12-17src/App.tsx - Using the useMutation Hook with Loading and Error

既然我们已经遇到了突变,让我们看看如何在前端使用它们。从useMutation()钩子中,我们析构了createInvoice(),现在我们可以调用这个函数来响应一些用户交互。在这种情况下,我们的组件中已经有了一个handleSubmit(),这是一个向数据库发送新数据的好地方。需要注意的是,突变会返回一个承诺。这意味着我们可以用then()/catch()/finally()try/catch/finally搭配async/await。我们能在突变中发送什么?更重要的是,我们如何使用它?一旦我们从钩子中获得了 mutator 函数,我们就可以通过提供一个 option 对象来调用它,这个对象至少应该包含变异变量。下面的例子说明了我们如何使用这种突变:

...
   await createInvoice({
     variables: {
       invoice: {
         user: 1,
         date: "2021-05-01",
         dueDate: "2021-05-31",
         state: "UNPAID",
         items: [
           {
             description: "Django consulting",
             price: 7000,
             taxed: true,
             quantity: 1,
           },
         ],
       },
     },
   });
...

在这个变异中,我们发送变异的整个输入类型,就像我们的模式中声明的那样。在这个例子中,我们硬编码了一些数据,但是在现实世界中,我们可能希望用 JavaScript 动态地获得突变变量。这正是我们在第六章中所做的,当时我们从一个带有FormData的表单中构建了一个POST有效载荷。让我们通过添加适当的输入和提交按钮来完成我们的表单。首先,清单 12-18 展示了完整的 React 表单(为了简洁,我们在这里忽略了任何 CSS 和风格方面的问题)。

<Form handleSubmit={handleSubmit}>
 <Select
   id="user"
   name="user"
   options={data.getClients}
 />
 <div>
   <label htmlFor="date">Date</label>
   <input id="date" name="date" type="date" required />
 </div>
 <div>
   <label htmlFor="dueDate">Due date</label>
   <input
     id="dueDate"
     name="dueDate"
     type="date"
     required
   />
 </div>
 <div>
   <label htmlFor="quantity">Qty</label>
   <input
     id="quantity"
     name="quantity"
     type="number"
     min="0"
     max="10"
     required
   />
 </div>

 <div>
   <label htmlFor="description">Description</label>
   <input
     id="description"
     name="description"
     type="text"
     required
   />
 </div>
 <div>
   <label htmlFor="price">Price</label>
   <input
     id="price"
     name="price"
     type="number"
     min="0"
     step="0.01"
     required
   />
 </div>
 <div>
   <label htmlFor="taxed">Taxed</label>
   <input id="taxed" name="taxed" type="checkbox" />
 </div>
 {mutationLoading ? (
   <p>Creating the invoice ...</p>
 ) : (
   <button type="submit">CREATE INVOICE</button>
 )}
</Form>

Listing 12-18src/App.tsx - The Complete Form

该表单包含创建新发票的所有输入。在底部,注意基于mutationLoading状态的条件渲染。

为了通知用户请求的状态,这是一件很好的事情。有了表单,我们就可以组装从handleSubmit()发出突变的逻辑了。为了方便起见,我们可以将async/awaittry/catch一起使用。看代码前的一些话:

  • 我们从一个FormData开始构建突变有效载荷

  • 在构建逻辑中,我们将quantity转换为整数,将taxed转换为布尔值

最后这些步骤是必要的,因为我们的 GraphQL 模式期望 quantity 是一个整数,而在表单中它只是一个字符串。清单 12-19 显示了完整的代码。

const handleSubmit = async (
 event: React.FormEvent<HTMLFormElement>
) => {
 event.preventDefault();
 if (event.target instanceof HTMLFormElement) {
   const formData = new FormData(event.target);

   const invoice = {
     user: formData.get("user"),
     date: formData.get("date"),
     dueDate: formData.get("dueDate"),
     state: "UNPAID",
     items: [
       {
         quantity: parseInt(
           formData.get("quantity") as string
         ),
         description: formData.get("description"),
         price: formData.get("price"),
         taxed: Boolean(formData.get("taxed")),
       },
     ],
   };

   try {
     const { data } = await createInvoice({
       variables: { invoice },
     });
     event.target.reset();
   } catch (error) {
     console.error(error);
   }
 }
};

Listing 12-19src/App.tsx - Logic for Sending the Mutation

除了FormData逻辑之外,其余部分非常简单:

  • 我们通过提供一个invoice有效载荷来发送带有createInvoice()的变异

  • 如果一切顺利,我们用event.target.reset()重置表单

如果我们在浏览器中测试,我们应该能够发出变异并从服务器得到响应。这个过程可以在浏览器的控制台中看到,如图 12-1 所示,其中突出显示了响应选项卡。

img/505838_1_En_12_Fig1_HTML.jpg

图 12-1

来自 GraphQL 服务器的变异响应

在 REST 中,当我们用一个POSTPATCH请求创建或修改一个资源时,API 用一个有效负载来响应。GraphQL 没有例外。事实上,我们可以访问突变的响应数据,如下面的代码片段所示:

const { data } = await createInvoice({
 variables: { invoice },
});
// do something with the data

在图 12-1 中,我们可以看到data对象包含一个名为createInvoice的属性,它保存了我们从变异中请求的字段。我们还可以看到__typename。这是 GraphQL 自省功能的一部分,它使得询问 GraphQL“这个对象是什么类型”成为可能?对 GraphQL 自省的解释超出了本书的范围,但是官方文档是了解更多信息的良好起点。

Note

这是提交到目前为止所做的更改并将工作推送到 Git repo 的好时机。你可以在 https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_12_graphql_strawberry 找到本章的源代码。

下一步是什么?

在最后几页中,我们仅仅触及了 GraphQL 的皮毛。题目比两章还大。以下是读完这本书后你可以自己探索的主题列表:

  • 认证和部署:在完全解耦的设置中,GraphQL 可以很好地使用 JWT 令牌进行认证。然而,该规范并不强制任何特定类型的身份验证方法。这意味着对 GraphQL API 使用基于会话的认证是可能的,正如我们在第十章中看到的 REST。

  • 订阅:Django 的 GraphQL Python 库可以与 Django 通道集成,通过 WebSocket 提供订阅。

  • 测试:测试 GraphQL API 不涉及任何魔法。因为它们接受并返回 JSON,所以任何用于 Python 或 Django 的测试 HTTP 客户端都可以用来测试 GraphQL 端点。

  • 排序、过滤和分页:使用 Django 和 Django REST 框架工具很容易对响应进行排序、过滤和分页。然而,要在 GraphQL 中实现同样的东西,我们需要手工编写一些代码。但是由于 GraphQL 查询接受参数,所以在 GraphQL API 中构建自定义过滤功能并不困难。

  • 性能 : 由于 GraphQL 中的查询可以嵌套,所以必须非常小心避免 N+1 个查询使我们的数据库崩溃。大多数 GraphQL 库包括一个所谓的数据加载器,它负责缓存数据库查询。

Exercise 12-1: Adding More Asynchronous Mutations

第六章中的线框有一个发送电子邮件按钮。尝试在 React 前端实现这一逻辑,并做一些改动。在后端,您还需要一个新的异步变异来发送电子邮件。

Exercise 12-2: Testing GraphQL

向这个简单的应用添加测试:可以用 Django 测试工具测试 GraphQL 端点,用 Cypress 测试接口。

摘要

在这一章中,我们用 GraphQL 和异步 Django 的基础知识结束了这个循环。您学习了如何:

  • 在后端和前端使用 GraphQL 变体

  • 使用异步 Django

现在轮到你了!去构建你的下一个 Django 项目吧!

额外资源