用Strawberry、FastAPI和Next.js使用GraphQL

964 阅读16分钟

大家好!👋

你可能听说过FastAPI、Strawberry、GraphQL之类的东西。现在,我们将向你展示如何在Next.js应用程序中把它们放在一起。我们将专注于用类型化的代码获得良好的开发者体验(DX)。大量的文章可以教你如何单独使用每一种,但关于把它们放在一起的资源并不多,特别是用Strawberry。

有多个基于Python的GraphQL库,它们彼此之间都略有不同。在很长一段时间里,Graphene是一个自然的选择,因为它是最古老的,而且在不同的公司被用于生产,但现在其他较新的库也开始获得一些关注。

我们将专注于这样一个名为Strawberry的库。它相对较新,需要Python 3.7以上版本,因为它利用了Python语言早期版本中没有的功能。它大量使用了数据类,并使用 mypy 进行完全类型化。

注意,你可以在GitHub上找到这篇文章的完整代码。

最后的产品。一个图书数据库

我们将有一个基本的项目结构,它将展示你如何成功地开始编写SQLAlchemy + Strawberry + FastAPI应用程序,同时利用类型和自动生成类型的React Hooks来利用你的GraphQL查询和Typescript代码中的突变。React Hooks将使用urql ,但你可以很容易地把它换成Apollo。

我将根据一个书店的想法创建数据库模式。我们将存储关于作者和他们的书的信息。我们不会使用React/Next.js创建一个完整的应用程序,但如果需要的话,我们会准备好所有必要的部件来完成。

我们的目标是通过到处使用类型和尽可能多地自动生成代码来获得更好的开发者体验。这将有助于在开发中捕获更多的bug。

这篇文章的灵感来自这个GitHub repo

开始使用

在我们开始工作之前,我们首先需要安装以下库/包。

  • Strawberry- 这是我们的GraphQL库,将在Python端提供GraphQL支持。
  • FastAPI- 这是我们的网络框架,用于服务我们基于Strawberry的GraphQL API。
  • Uvicorn- 这是一个ASGI网络服务器,将在生产中为我们的FastAPI应用提供服务。
  • Aiosqlite- 它为SQLite提供异步支持
  • SQLAlchemy- 这是我们的ORM,用于处理SQLite DB。

让我们创建一个新的文件夹并使用pip安装这些库。我不会手动创建新文件夹,而是使用 create-next-app命令来创建它。我们将把这个命令创建的文件夹视为我们整个项目的根文件夹。这只是使解释更容易。我将在后面讨论所需的JS/TS库。现在,我们将只关注Python方面。

确保你的系统有create-next-app ,作为一个有效的命令。一旦你做了,在终端运行以下命令。

$ npx create-next-app@latest --typescript strawberry_nextjs

上述命令应该创建一个strawberry_nextjs 文件夹。现在进入该文件夹并安装所需的基于Python的依赖。

$ cd strawberry_nextjs

$ python -m venv virtualenv
$ source virtualenv/bin/activate

$ pip install 'strawberry-graphql[fastapi]' fastapi 'uvicorn[standard]' aiosqlite sqlalchemy

Strawberry + FastAPI: Hello, world!

让我们从一个 "你好,世界!"的例子开始,它将向我们展示组成草莓应用程序的点点滴滴。创建一个名为app.py的新文件,并在其中添加以下代码。

import strawberry

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

authors: list[str] = []

@strawberry.type

让我们分块看一下这段代码。我们首先导入所需的库和包。我们创建一个作者列表,作为我们的临时数据库,保存作者姓名(我们将简要地创建一个实际的数据库)。

然后我们创建查询类,并用strawberry.type装饰器来装饰它。这将它转换为GraphQL类型。在这个类中,我们定义一个all_authors 解析器,从列表中返回所有的作者。一个解析器也需要说明它的返回类型。我们将在下一节中研究如何定义稍微复杂的类型,但现在,一个字符串列表就足够了。

接下来,我们创建一个新的Mutation 类,其中包含所有的GraphQL突变。现在,我们只有一个简单的add_author 变异,它接收一个名字并将其添加到作者列表中。

然后,我们将查询和突变类传递给strawberry.Schema ,以创建一个GraphQL模式,然后将其传递给GraphQLRouter 。最后,我们将GraphQLRouter ,并让GraphQLRouter处理所有传入/graphql端点的请求。

如果你不知道这些术语是什么意思,那么让我给你快速复习一下。

  • 查询--向服务器发送的检索数据/记录的一种请求类型
  • 突变- 发送给服务器的一种请求,用于创建/更新/删除数据/记录
  • 类型 - 我们在GraphQL中交互的对象。这些代表了数据/记录/错误以及两者之间的一切。
  • 解析器- 一个为我们的模式中的单个字段填充数据的函数。

你可以在官方文档页面上阅读更多关于Strawberry模式的基础知识。

要运行这段代码,请跳到终端,执行以下命令。

$ uvicorn app:app --reload --host '::'

这应该会打印出类似以下的输出。

INFO:将观察这些目录中的变化。['/Users/yasoob/Desktop/strawberry_nextjs']
INFO:Uvicorn在http://[::]:8000上运行(按CTRL+C退出)
INFO:开始使用watchgod的reloader进程[56427]
INFO:启动服务器进程 [56429]
INFO:等待应用程序启动。
INFO:应用程序启动完成。

现在去https://127.0.0.1:8000/graphql,你应该会看到交互式的GraphiQL游乐场。

Interactive GraphiQL Playground

试着执行这个查询。

query MyQuery {
allAuthors
}

这应该输出一个空列表。这是预期的,因为我们的列表中没有任何作者。然而,我们可以通过先运行一个突变,然后再运行上述查询来解决这个问题。

要创建一个新的作者,运行addAuthor 突变。

mutation MyMutation {
addAuthor(name: "Yasoob")
}

现在如果你运行allAuthors查询,你应该在输出列表中看到Yasoob。

{
"data": {
"allAuthors": [
"Yasoob"
]
}
}

你现在可能已经意识到了这一点,但是Strawberry在内部自动将我们的camel_case字段转换为PascalCase字段,这样我们就可以遵循惯例,在GraphQL API调用中使用PascalCase,在Python代码中使用camel_case。

有了这些基础知识,让我们开始着手开发我们的书店型应用程序。

定义模式

我们需要弄清楚的第一件事是我们的模式是什么。我们需要为我们的应用程序定义哪些查询、突变和类型。

在这篇文章中,我不会关注GraphQL的基础知识,而是只关注Strawberry的具体部分。正如我已经提到的,我们将遵循一个书店的想法。我们将为作者和他们的书存储数据。这就是我们的数据库最后的模样。

Defining Schema

定义SQLAlchemy模型

我们将使用SQLAlchemy,所以让我们把我们的两个模型定义为类。我们将使用Async SQLAlchemy。在strawberry_nextjs 文件夹中创建一个新的models.py 文件,并向其添加以下导入。

import asyncio
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Optional

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker

这些导入稍后就会有意义。我们将使用继承自SQLAlchemy的声明性基础模型的类来声明性地定义我们的模型。SQLAlchemy为我们提供了declarative_base() 函数来获取声明性基础模型。让我们使用它并定义我们的模型。

Base = declarative_base()


Our Author class定义了两列: , 只是一个关系属性,帮助我们浏览模型之间的关系,但没有作为一个单独的列存储在作者表中。我们回将书籍属性填充为作者。这意味着我们可以访问 ,以访问一本书的链接作者。id name. books book.author

The Book 这类与作者类非常相似。我们定义一个额外的 ,将作者和书联系起来。与关系不同的是,它被存储在图书表中。而且我们还将 属性回填为 。这样我们就可以像这样访问某个特定作者的书: 。author_idcolumn author books author.books

现在我们需要告诉SQLAlchemy使用哪个数据库以及在哪里找到它。

engine = create_async_engine(
"sqlite+aiosqlite:///./database.db", connect_args={"check_same_thread": False}
)

我们使用aiosqlite 作为连接字符串的一部分,因为aiosqlite 允许SQLAlchemy以异步的方式使用SQLiteDB。我们传递check_same_thread 参数,以确保我们可以在多个线程中使用相同的连接。

以多线程的方式使用SQLite是不安全的,因为没有采取额外的措施确保数据不会在并发的写操作中被破坏,所以建议在生产中使用PostgreSQL或类似的高性能DB。

接下来,我们需要创建一个会话。

async_session = sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)

而为了确保我们在每次交互时正确地关闭会话,我们将创建一个新的上下文管理器。

@asynccontextmanager

我们也可以在没有上下文管理器的情况下使用会话,但这将意味着我们必须在每次使用会话后手动关闭会话。

最后,我们需要确保我们已经创建了新的数据库。我们可以在models.py 文件中添加一些代码,如果我们试图直接执行models.py 文件,它将使用我们声明的模型创建一个新的DB文件。

async

这将放弃我们DB中的所有现有表,并根据这个文件中定义的模型重新创建它们。我们可以添加保障措施,确保我们不会意外地删除我们的数据,但这已经超出了本文的范围。我只是想向你展示一切是如何联系在一起的。

现在我们的models.py 文件已经完成,我们准备定义我们的StrawberryAuthorBook 类型,它们将映射到SQLAlchemy模型上。

定义Strawberry类型

当你读到这篇文章的时候,Strawberry可能已经对直接使用SQLAlchemy模型有了稳定的内置支持,但是现在,我们必须定义自定义的Strawberry类型,以映射到SQLAlchemy模型上。让我们先定义这些类型,然后再了解它们是如何工作的。把这段代码放在app.py 文件中。

import models

...

@strawberry.type
classAuthor:
id: strawberry.ID
name: str

@classmethod
defmarshal(cls, model: models.Author) -/>"Author":
returncls(id=strawberry.ID(str(model.id)), name=model.name)

@strawberry.type
classBook:
id: strawberry.ID
name: str
author:Optional[Author] =None

@classmethod
defmarshal(cls, model: models.Book) -\>"Book":
returncls(
id=strawberry.ID(str(model.id)),
name=model.name,
author=Author.marshal(model.author)ifmodel.authorelseNone,
)

为了定义一个新的类型,我们简单地创建一个类,并用strawberry.type装饰器来装饰它。这与我们如何定义MutationQuery 类型非常相似。唯一不同的是,这次我们不会将这些类型直接传递给strawberry.Schema ,所以Strawberry不会将它们当作MutationQuery 类型。

每个类都有一个marshal方法。这个方法允许我们接受一个SQLAlchemy模型并从中创建一个Strawberry类型类实例。Strawberry使用strawberry.ID 来表示一个对象的唯一标识符。Strawberry默认提供了一些标量类型,其工作原理与strawberry.ID 。这取决于我们如何使用这些类型将我们的SQLAlchemy数据映射到这个自定义类型类属性。我们一般会尝试找到与SQLAlchemy列类型最接近的替代品并使用它。

Book 类中,我还向你展示了如何将一个类型属性标记为可选,并提供一个默认值。我们把作者标记为可选的。这只是为了向你展示它是如何做到的,以后;我将把它标记为必需。

另一件需要注意的事情是,我们也可以为我们的突变和查询调用定义一个返回类型列表。这可以确保我们的GraphQL API消费者可以根据它收到的返回类型适当地处理输出。如果你了解GraphQL,那么这就是我们定义片段的方式。让我们首先定义这些类型,然后我会告诉你,一旦我们开始定义我们的新突变和查询类,如何使用它们。

@strawberry.type

我们基本上是说,我们的AddBookResponseAddAuthorResponse 类型是联合类型,可以是元组中列出的三种(或两种)类型中的任何一种。

定义查询和变异

现在让我们来定义我们的查询。我们将只有两个查询。一个是列出所有的书,一个是列出所有的作者。

from sqlalchemy import select


这里似乎发生了很多事情,所以让我们把它分解。

首先,看一下书籍解析器。我们使用get_session 上下文管理器来创建一个新的会话。然后我们创建一个新的SQL语句,选择Book模型,并根据书名对它们进行排序。之后,我们使用之前创建的会话执行SQL语句,并将结果放入db_books 。最后,我们把每本书都编入一个Strawberry Book类型,并把它作为输出返回。我们还将books解析器的返回类型标记为Books 的一个list

authors 解析器与books 解析器非常相似,所以我不需要解释。

现在让我们写一下我们的突变。

@strawberry.type

突变是相当直接的。让我们从add_book 的突变开始。

add_book 这个突变把书的名字和作者的名字作为输入。我将 定义为可选参数,只是为了告诉你如何定义可选的参数,但是在方法体中,如果没有传入 ,我将返回 ,从而强制要求 的存在。author_name author_name AuthorNameMissing author_name

我根据传入的author_name ,在db 中过滤Authors ,并确保有一个指定名字的作者存在。否则,我将返回AuthorNotFound 。如果这两个检查都通过了,我就创建一个新的models.Book 实例,通过会话将其添加到db ,并提交。最后,我返回一个经过修饰的书作为返回值。

add_author 这和 几乎是一样的,所以没有理由再去看一遍代码。add_book

我们几乎完成了草莓方面的工作,但我有一个额外的东西要分享,那就是数据加载器。

GraphQL的另一个(并不总是)有趣的功能是递归解析器。你在上面看到,在Book的marshal 方法中,我也定义了author 。这样我们就可以像这样运行一个GraphQL查询。

query {
book {
author {
name
}
}
}

但是,如果我们想运行这样的查询呢。

query {
author {
books {
name
}
}
}

这就不行了,因为我们还没有在我们的草莓类型上定义一个books属性。让我们重写我们的Author 类,并在我们的类方法中为Strawberry提供的默认上下文添加一个DataLoader

from strawberry.dataloader import DataLoader


让我们从下往上理解这个问题。草莓允许我们通过一个上下文将自定义函数传递给我们的类(那些用@strawberry.type )方法。这个上下文是在单个请求中共享的。

DataLoader允许我们批量处理多个请求,这样我们就可以减少对db 的来回调用。我们创建一个DataLoader 实例,并告知它如何从db 为传入的作者加载书籍。我们把这个DataLoader 放在一个字典里,并把它作为context_getter 的参数传递给GraphQLRouter 。这使得这个字典可以通过info.context 供我们的类方法使用。我们用它来加载每个作者的书。

在这个例子中,DataLoader并不是非常有用。当我们用一个参数列表来调用DataLoader ,它的主要好处就会显现出来。这大大减少了对数据库的调用。而且DataLoaders 也会缓存输出,它们在一次请求中被共享。因此,如果你在一次请求中多次向数据加载器传递相同的参数,将不会导致额外的数据库点击。超级强大!

测试一下Strawberry

一旦你做了这些代码修改并保存,uvicorn实例应该自动重新加载。转到http://127.0.0.1:8000/graphql,测试一下最新的代码。

试着执行下面的突变两次。

mutation Author {
addAuthor(name: "Yasoob") {
... on Author {
id
name
}
... on AuthorExists{
message
}
}
}

第一次,它应该输出这个。

{
"data": {
"addAuthor": {
"id": "1",
"name": "Yasoob"
}
}
}

而第二次应该输出这个。

{
"data": {
"addAuthor": {
"message": "Author with this name already exist"
}
}
}

Strawberry Output

现在让我们试着添加新书。

mutation Book {
addBook(name: "Practical Python Projects", authorName: "Yasoob") {
... on Book {
id
name
}
}
}

Python/Strawberry Side

很好!我们的Python/Strawberry端工作得非常好。但现在我们需要在Node/Next.js端将其连接起来。

设置Node的依赖性

我们将使用graphql-codegen ,为我们自动创建类型化的钩子。所以基本的工作流程是,在我们可以在Typescript代码中使用GraphQL查询、突变或片段之前,我们将在GraphQL文件中定义它。然后,graphql-codegen 将介绍我们的Strawberry GraphQL API,并创建类型,使用我们自定义的GraphQL查询/突变/片段来创建自定义的urql 钩子。

urql 是一个相当全功能的React的GraphQL库,使与GraphQL API的交互变得更加简单。通过这一切,我们将减少自己在编码类型化钩子方面的大量努力,然后才能在我们的Next.js/React应用程序中使用GraphQL API。

在我们继续前进之前,我们需要安装一些依赖项。

$ npm install graphql
$ npm install @graphql-codegen/cli
$ npm install @graphql-codegen/typescript
$ npm install @graphql-codegen/typescript-operations
$ npm install @graphql-codegen/typescript-urql
$ npm install urql

在这里,我们要安装urql 和一些用于@graphql-codegen 的插件。

设置graphql-codegen

现在我们将在我们项目的根部创建一个codegen.yml 文件,它将告诉graphql-codegen 做什么。

overwrite: true
schema: "http://127.0.0.1:8000/graphql"
documents: './graphql/**/*.graphql'
generates:
graphql/graphql.ts:
plugins:

    • "typescript"
    • "typescript-operations"
    • "typescript-urql"

我们正在通知graphql-codegen,它可以在http://127.0.0.1:8000/graphql 找到我们的GraphQL API的模式我们还告诉它(通过documents 键),我们已经在位于graphql 文件夹的 graphql 文件中定义了我们的自定义片段、查询和突变。然后我们指示它通过三个插件运行模式和文件来生成graphql/graphql.ts 文件。

现在在我们的项目目录下建立一个graphql 文件夹,并在其中创建一个新的operations.graphql 文件。我们将定义所有的片段、查询和突变,我们计划在我们的应用程序中使用。我们可以为所有这三个文件创建单独的文件,graphql-codegen ,在处理时将自动合并它们,但我们将保持简单,暂时将所有东西放在一个文件中。让我们把下面的GraphQL添加到operations.graphql

query

这与我们在GraphiQL在线界面中执行的代码非常相似。这个GraphQL代码将告诉graphql-codegen ,它需要为我们产生哪些urql 变异和查询钩子。

有人讨论过graphql-codegen 通过内省我们的在线GraphQL API来生成所有的突变和查询,但到目前为止,只使用graphql-codegen 是不可能做到的。确实有一些工具允许你这样做,但我不打算在这篇文章中使用它们。你可以自己去探索它们。

接下来让我们编辑package.json文件,添加一个命令,通过npm运行graphql-codegen 。在scripts 部分添加这段代码。

"codegen": "graphql-codegen --config codegen.yml"

现在我们可以到终端运行graphql-codegen。

$ npm run codegen

如果命令成功,你应该在graphql 文件夹里有一个graphql.ts 文件。我们可以继续前进,在我们的Next代码中使用生成的urql 钩子,像这样。

import {
useAuthorsQuery,
} from "../graphql/graphql";

// ....

const [result] = useAuthorsQuery(...);

你可以在这里阅读更多关于graphql-codegen urql 的插件。

解决 CORS 问题

在生产环境中,你可以从相同的域+PORT为GraphQL API和Next.js/React应用提供服务,这将确保你不会遇到CORS问题。对于开发环境,我们可以在next.config.js 文件中添加一些代理代码,指示 NextJS 将所有对/graphql 的调用代理到运行在不同端口的uvicorn

/**

这将确保你在本地开发中也不会遇到任何 CORS 问题。

总结

我希望你能从这篇文章中学到一两点东西。我特意没有对任何一个主题进行太详细的介绍,因为网上已经有这样的文章了,但很难找到一篇文章来告诉你所有的东西是如何连接在一起的。

你可以在我的GitHub上找到这篇文章的所有代码。在未来,我可能会创建一个完整的项目,向你展示一个更具体的例子,说明你如何在你的应用程序中使用生成的代码。同时,你可以看看这个 repo,它是这篇文章的灵感来源。Jokull可能是第一个公开主持一个结合所有这些不同工具的项目的人。谢谢你,Jokull!

另外,如果你有任何Python或Web开发项目,请联系我,hi@yasoob.me,分享你的想法。我做了相当多的项目,所以几乎没有什么是不寻常的。让我们一起创造一些了不起的东西。😄

回头见!👋 ❤

参考资料。

The postUsing GraphQL with Strawberry, FastAPI, and Next.jsappeared first onLogRocket Blog.