原文:zh.annas-archive.org/md5/bd742fc2af9d0b64a6f94d7b9da9035f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

全栈 FastAPI、React 和 MongoDB,第二版是一本快速、简洁、实用的入门指南,旨在提升 Web 开发者的潜力,并帮助他们利用 FARM 栈的灵活性、适应性和稳健性,在快速发展的 Web 开发和 AI 领域中保持领先。本书介绍了栈的每个元素,然后解释了如何使它们协同工作以构建中型 Web 应用程序。

本书通过实际操作示例和真实世界的用例,展示了如何使用 MongoDB 设置文档存储,使用 FastAPI 构建简单的 API,以及使用 React 创建应用程序。此外,它深入探讨了使用 Next.js,确保 MongoDB 中的数据完整性和安全性,以及将第三方服务与应用程序集成。

这本书将如何帮助您

这本书采用实际操作的方法,通过使用 FARM 栈的真实世界示例来展示 Web 应用程序开发。到本书结束时,您将能够自信地使用 FARM 栈以快速的速度开发功能齐全的 Web 应用程序。

这本书面向的对象是谁

这本书适合具有基本 JavaScript 和 Python 知识的初级 Web 开发者,他们希望提高自己的开发技能,掌握一个强大且灵活的栈,并更快地编写更好的应用程序。

这本书涵盖了哪些内容

第一章Web 开发与 FARM 栈,通过快速浏览广泛使用的各种技术,为您提供了对 Web 开发领域的深入理解。它介绍了最受欢迎的选项——FARM 栈。它突出了 FARM 栈组件的优势,它们之间的关系,以及为什么这一组特定技术非常适合 Web 应用程序。

第二章使用 MongoDB 设置数据库,提供了 MongoDB 的概述,然后展示了如何为 FARM 应用程序设置数据存储层。它帮助您了解创建、更新和删除文档的基本知识。此外,本章详细介绍了聚合管道框架——一个强大的分析工具。

第三章Python 类型提示和 Pydantic,包括一些示例,教您更多关于 FastAPI 的 Web 特定方面以及如何无缝地在 MongoDB、Python 数据结构和 JSON 之间混合数据。

第四章FastAPI 入门,专注于介绍 FastAPI 框架,以及标准的 REST API 实践及其在 FastAPI 中的实现方式。它涵盖了 FastAPI 实现最常见 REST API 任务的一些非常简单的示例,以及它如何通过利用现代 Python 功能和库(如 Pydantic)来帮助您。

第五章设置 React 工作流程,展示了如何使用 React 框架设计一个由几个组件组成的应用程序。它讨论了探索 React 及其各种功能所需的工具。

第六章身份验证和授权,详细介绍了基于 JSON Web TokensJWTs)的简单、健壮且可扩展的 FastAPI 后端配置。它展示了如何将基于 JWT 的身份验证方法集成到 React 中,利用 React 的强大功能——特别是 Hooks、Context 和 React Router。

第七章使用 FastAPI 构建 Backend,帮助您处理一个简单的业务需求并将其转化为一个完全功能、部署在互联网上的 API。它展示了如何定义 Pydantic 模型、执行 CRUD 操作、构建 FastAPI 后端并连接到 MongoDB。

第八章构建应用程序的前端,说明了构建全栈 FARM 应用程序前端的步骤。它展示了如何使用现代 Vite 设置创建 React 应用程序并实现基本功能。

第九章使用 FastAPI 和 Beanie 集成第三方服务,介绍了 Beanie,这是一个基于 Motor 和 Pydantic 的流行 ODM 库,用于 MongoDB。它展示了如何定义模型和映射到 MongoDB 集合的 Beanie 文档。您将看到如何构建另一个 FastAPI 应用程序,并使用后台任务集成第三方服务。

第十章使用 Next.js 14 进行 Web 开发,对重要的 Next.js 概念进行了概述,例如服务器操作、表单处理和 Cookie,以帮助创建新的 Next.js 项目。您还将学习如何在 Netlify 上部署您的 Next.js 应用程序。

第十一章有用的资源和项目想法,在工作与 FARM 栈时提供了一些实用建议,以及 FARM 栈或非常相似的栈可能适用且有帮助的项目想法。

为了充分利用本书

您需要了解 JavaScript 和 Python 的基础知识。对 MongoDB 的先验知识更佳,但不是必需的。您将需要以下软件:

本书涵盖的软件/硬件 操作系统要求
MongoDB 版本 7.0 或更高 Windows、macOS 或 Linux
MongoDB Atlas Search Windows、macOS 或 Linux
MongoDB Shell 2.2.15 或更高版本 Windows、macOS 或 Linux
Node.js 版本 18.17 或更高 Windows、macOS 或 Linux
Python 3.11.7 或更高版本 Windows、macOS 或 Linux
Next.js 14 或更高版本 Windows、macOS 或 Linux
FastAPI 0.111.1 Windows、macOS 或 Linux
React 18 或更高版本 Windows、macOS 或 Linux

如果您使用的是本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub(github.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB-2nd-Edition)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/Bookname_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“可选地,您可以创建一个middleware.js函数,该函数将包含将在每个(或仅选定的)请求上应用的中间件。”

代码块设置如下:

const Cars = () => {
    return (
        <div>Cars</div>
    )
}
export default Cars

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

      <body>
        <Navbar />
        {children}
      </body>

任何命令行输入或输出都应如下编写:

git push -u origin main

粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“选择 Windows 版本,然后点击下载。”

小贴士或重要注意事项

看起来是这样的。

联系我们

我们欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版: 如果您在互联网上发现我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法随身携带您的印刷书籍吗?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您还可以获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容。

按照以下简单步骤获取这些好处:

  1. 扫描二维码或访问以下链接

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_QR_Free_PDF.png

packt.link/free-ebook/9781835886762

  1. 提交您的购买证明

  2. 就这样!我们将直接将免费 PDF 和其他优惠发送到您的电子邮件。

第一章:Web 开发与 FARM 栈

网站是使用一组被称为的技术构建的。栈的每个组件都负责应用的一层。虽然在理论上,你可以将任何类型的 frontend 技术与任何类型的 backend 技术结合,从而最终得到一个自定义栈,但一些栈在敏捷性和减少开发时间方面已经证明了自己的价值。如果你是一位需要不时将一些数据上线到网上的 Web 开发者或分析师,或者你只是想拓宽你的开发者视野,那么这一章应该会给你一些关于这组工具的视角,以及它们与替代技术的比较。

本章从可用技术和需求的角度概述了当今的 Web 开发格局,并在本章末尾,我们将论证使用FARM栈的必要性——这是一个结合了FastAPI用于 REST API 层、React用于前端和MongoDB作为数据库的栈。

本书专注于构成 FARM 栈的技术的高层次概念。通过学习这些概念,你将能够以快速的速度和现代的能力开发你的下一个 Web 开发项目。目前,我们不会深入细节或具体示例,而是将选定的栈组件(MongoDB、FastAPI 和 React)与它们的可能对应物进行比较。

到本章结束时,你将很好地理解 FARM 栈各个组件为开发项目带来的好处,它们之间的关系,以及为什么这一套技术非常适合具有灵活规格的 Web 应用——无论是从处理的数据还是期望的功能性来看。

本章将涵盖以下主题:

  • FARM 栈是什么以及组件是如何相互配合的?

  • 为什么使用 MongoDB 进行数据存储?

  • 什么是 FastAPI?

  • 前端——React

技术要求

对于这本书,你需要一些东西来帮助你完成你的旅程。以下是一些建议:

让我们从对 FARM 栈的基本理解开始。

FARM 栈是什么?

栈是一组覆盖现代 Web 应用不同部分的技术,混合并良好集成。正确的栈将使你在构建 Web 应用时能够满足某些标准,而所需的工作量和时间将比从头开始构建要少得多。

首先,让我们看看你需要构建一个功能性的 Web 应用需要什么:

  • 操作系统:通常,这是基于 Unix/Linux 的。

  • 存储层:一个 SQL 或 NoSQL 数据库。在这本书中,我们将使用 MongoDB。

  • Web 服务器:Apache 和 NGINX 相当受欢迎,但我们将讨论 FastAPI 的 Python 解决方案,如 Uvicorn 或 Hypercorn。

  • 开发环境:Node.js/JavaScript、.NET、Java 或 Python。

可选的,并且通常是,你还可以添加一个前端库或框架(例如 Vue.js、Angular、React 或 Svelte),因为绝大多数的 Web 开发公司从采用一个框架中受益匪浅,无论是在一致性、开发速度还是符合标准方面。此外,用户期望随着时间的推移而改变。对于登录、按钮、菜单和其他网站元素应该是什么样子,以及它们应该如何工作,存在一些未言明的标准。使用框架将使你的应用程序与现代 Web 更加一致,并且对用户满意度大有裨益。

最著名的堆栈如下:

  • MERNMongoDB + Express.js + React + Node.jsMERN)可能是当今最受欢迎的堆栈之一。开发者可以舒适地使用 JavaScript,除非他们需要编写一些样式表。随着 React Native 用于移动应用和 Electron.js 用于桌面应用,一个产品几乎可以涵盖每一个平台,同时仅依赖于 JavaScript。

  • MEANMongoDB + Express.js + Angular.js + Node.jsMEAN)与之前提到的 MERN 类似,Angular.js 以前端以更结构化的模型-视图-控制器MVC)方式管理。

  • LAMPLinux + Apache + MySQL + PHPLAMP)可能是第一个流行起来的堆栈缩写,也是过去 20 年中使用最广泛的之一。它至今仍然非常受欢迎。

前两个堆栈运行在 Node.js 平台上(一个服务器端运行的 JavaScript V8 引擎),并且有一个共同的 Web 框架。尽管 Express.js 是最受欢迎的,但在 Node.js 宇宙中还有许多优秀的替代品,例如 Koa.js、Fastify.js,或者一些更结构化的,如 Nest.js。

由于这是一本 Python 书,我们还将介绍一些重要的 Python 框架。对于 Python 开发者来说,最受欢迎的前三个框架是DjangoFlaskFastAPI。使用 Django Web 框架和优秀的Django REST FrameworkDRF)以现代和逻辑的方式构建 REST API 非常流行。Django 本身在 Python 开发者中非常成熟且广为人知。它还包含一个管理站点,可以自定义和序列化 REST 响应,可以选择功能性和基于类的视图,等等。

另一方面,FastAPI 是一个相对较新的框架。首次发布于 2018 年 12 月,这个替代的轻量级框架迅速获得了支持者。几乎立即,这些支持者就在技术堆栈中为 FastAPI 创造了一个新的缩写——FARM

让我们了解 FARM 代表什么:

  • FA代表 FastAPI——在技术年数中,一个全新的 Python Web 框架

  • R 代表 React,这是最受欢迎的 UI 库

  • M 代表数据层——MongoDB,这是目前最流行的 NoSQL 数据库

图 1.1 提供了 FARM 栈中各个组成部分之间集成的高级概述:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_01_01.png

图 1.1:FARM 栈及其组件

如前图所示,FARM 栈由三层组成:

  1. 用户通过客户端执行操作,在我们的案例中,这将是基于 React 的——这最终创建了一个包含 HTML、层叠样式表(CSS)和 JavaScript 的包。

  2. 此用户操作(如鼠标点击、表单提交或其他事件)随后触发一个 HTTP 请求(如 GETPOSTPUT 或带有有效负载的其他 HTTP 动词)。

  3. 最后,此请求由 REST API 服务(FastAPI)处理。

Python 部分以 FastAPI 和可选依赖为中心,并由 findOnefindcreateupdate 等操作以及 MongoDB 聚合框架提供服务。从数据库中获得的结果通过 FastAPI 的 Python 驱动(Motor)进行解释,从 BSON 转换为适当的 Python 数据结构,并最终以纯 JSON 的形式从 REST API 服务器输出。如果你使用 Motor,这是一个 MongoDB 的异步 Python 驱动程序,这些调用将以异步方式处理。

最后,回到 图 1.1 中的图和标有 JSON 的箭头,数据被输入到 UI 中,由 React 处理,并用于更新界面、渲染必要的组件以及将 UI 与 React 的虚拟 DOM 树同步。

接下来的几节将讨论 FARM 栈诞生的动机。为什么选择这些技术,更重要的是,为什么选择这些技术组合?你将详细了解每个组件及其使其成为现代 Web 开发工作流程良好匹配的功能。在简要介绍整个栈的好处之后,这些章节将提供每个选择的概述,并强调它可以为现代 Web 开发工作流程提供的优势。

为什么选择 FARM 栈?

栈的灵活性和简单性,以及其组件,在开发速度、可扩展性和可维护性方面提供了真正的提升,同时允许未来的可扩展性(由于 MongoDB 的分布式特性以及 FastAPI 的异步特性),如果您的产品需要发展并变得比最初预期的更大,这可能至关重要。理想的情况可能是一个可以实验的小到中型规模的 Web 应用程序。

开发人员和分析师都可以从 Python 的生态系统和扩展性中受益,这个生态系统包含几乎涵盖所有包含某种类型计算的人类活动的丰富模块。

为什么使用 MongoDB?

MongoDB 是一个免费、快速且可扩展的数据库,具有 JSON 格式和简单语法。它支持灵活的模式,从而实现迭代和快速开发。MongoDB 能够适应各种复杂性的数据结构。此外,其查询和聚合方法使其成为像 FastAPI 这样的灵活 REST API 框架的绝佳选择,结合官方 Python 驱动程序如 Motor。它具有高度的采用率和成熟度,并且是十年前席卷 Web 开发世界的 NoSQL 数据存储运动支柱之一。

以下是一些将在本书中详细说明的其他功能:

  • 复杂的嵌套结构:MongoDB 文档允许嵌入其他文档和文档数组,这自然地转化为现代数据网络应用程序的数据流(例如,可以将所有评论嵌入到它们所响应的博客文章中)。鼓励去规范化。

  • 简单直观的语法:执行基本 创建读取更新删除CRUD)操作的方法,结合强大的聚合框架和投影,通过使用驱动程序,几乎可以轻松实现所有数据读取。对于有 SQL 经验的人来说,这些命令应该是直观的。

  • 社区和文档:MongoDB 由一家成熟的公司和一个强大的社区支持,并提供各种工具以促进开发和原型设计过程。例如,Compass 是一个桌面应用程序,它允许用户管理和维护数据库。无服务器函数的框架正在不断更新和升级,并且几乎为每种编程语言都提供了优秀的驱动程序。

当然,MongoDB 不是一个万能的解决方案,一些挑战在开始时就值得注意。一方面,无模式设计和将任何类型的数据插入数据库的能力可能会引起一些恐慌,但这转化为后端需要更强的数据完整性验证。你将看到 Pydantic——一个优秀的 Python 验证和类型强制库——如何帮助你实现更强的数据完整性。在 SQL 世界中存在的复杂连接的缺失,可能是一些应用程序的致命缺陷。

现在你已经了解了 MongoDB 在可扩展性和灵活性方面的优势,以及其无模式的方法,那么请看看你选择的 REST API 框架 FastAPI,并学习它是如何帮助你实现无模式方法并简化与数据的交互的。

为什么使用 FastAPI?

FastAPI 是一个现代且性能卓越的 Web 框架,用于构建 API。由 Sebastian Ramirez 构建,它使用了 Python 编程语言的新特性,如类型提示和注解、async – await 语法、Pydantic 模型、WebSocket 支持,等等。

如果你不太熟悉 API,让我们深入了解,通过了解 API 是什么来开始。应用程序编程接口API)用于实现不同软件组件之间的某种交互,它们通过请求和响应的周期使用超文本传输协议HTTP)进行通信。因此,API 如其名所示,是一个接口。通过这个接口,人类或机器与应用程序或服务进行交互。每个 API 提供商都应该有一个适合他们提供的数据类型的接口;例如,一个天气预报站提供的 API 会列出某个地点的温度和湿度水平。体育网站提供正在进行的比赛的统计数据。一个比萨饼配送 API 会提供所选配料、价格和预计送达时间。

API 涉及到你生活的方方面面,例如,传输医疗数据、实现应用程序之间的快速通信,甚至在田野中的拖拉机上使用。API 是使今天的网络运行的原因,简单来说,是信息交换的最佳形式。

本章不会详细讲解 REST API 的严格定义,而是列出它们的一些最重要的特性:

  • 无状态:据说 REST API 是无状态的,这意味着客户端和服务器之间不存储任何状态。所有请求和响应都由 API 服务器独立处理,且不涉及会话本身的信息。

  • 分层结构:为了保持 API 可扩展性和可理解性,RESTful 架构意味着一个分层结构。不同的层形成一个层次结构,相互通信但不与每个组件通信,从而提高了整体安全性。

  • 客户端-服务器架构:API 应该能够连接不同的系统/软件组件,而不限制它们自身的功能——服务器和客户端必须保持相互独立。

虽然与其他 Python 框架相比较新,但 MongoDB 选择 FastAPI 作为他们的 REST API 层有许多原因。以下是其中的一些原因:

  • 高性能:FastAPI 可以实现非常高的性能,尤其是与其他基于 Python 的解决方案相比。通过底层使用 Starlette,FastAPI 的性能达到了通常只属于 Node.js 和 Go 的水平。

  • 数据验证和简洁性:由于 Pydantic 极度依赖 Python 类型,这带来了许多好处。由于 Pydantic 结构只是开发者定义的类的实例,你可以使用复杂的数据验证、深度嵌套的 JSON 对象和分层模型(使用 Python 列表和字典),这与 MongoDB 的本质非常契合。

  • 快速开发:有了强大的集成开发环境IDE)支持,开发变得更加直观,这导致开发时间更短,错误更少。

  • 标准兼容性:FastAPI 基于标准,完全兼容用于构建 API 的开放标准——如 OpenAPI 和 JSON 模式。

  • 应用逻辑结构化:该框架允许将 API 和应用程序结构化为多个路由器,允许对请求和响应进行细粒度定制,并轻松访问 HTTP 周期的每个部分。

  • asyncio模块集成到 Python 中。

  • 依赖注入:FastAPI 中的依赖注入系统是其最大的卖点之一。它使得创建复杂的功能变得容易重用,这些功能可以在你的 API 中轻松使用。这是一件大事,可能是使 FastAPI 成为混合 Web 应用理想的特性——它为开发者提供了将不同功能轻松附加到 REST 端点的机会。

  • 优秀的文档:该框架本身的文档非常出色,无与伦比。它既易于遵循,又内容丰富。

  • 自动文档生成:基于 OpenAPI,FastAPI 能够实现自动文档的创建,这本质上意味着你可以免费使用 Swagger 来获取你的 API 文档。

此外,入门相对简单:

pip install fastapi

为了至少对使用 FastAPI 进行编码有一个基本的概念,让我们看看一个最小化的 API:

# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get(/)
async def root():
    return {“message”: “Hello World”}

前几行代码定义了一个具有单个端点(/)的最小 API,该端点对GET请求返回消息Hello world。你可以实例化一个 FastAPI 类,并使用装饰器告诉服务器哪些 HTTP 方法应该触发哪个函数以进行响应。

Python 和 REST API

Python 已经用于构建 REST API 很长时间了。尽管有许多选项和解决方案,DRFFlask似乎是最受欢迎的,至少直到最近。如果你喜欢冒险,你可以通过 Google 搜索不那么流行或较旧的框架,例如bottle.pyCherryPy

DRF 是 Django Web 框架的插件系统,它使 Django 系统能够创建高度定制的 REST API 响应,并基于定义的模型生成端点。DRF 是一个非常成熟且经过实战检验的系统。它定期更新,其文档非常详细。

Flask,Python 的轻量级微框架,是网络构建 Python 工具中的瑰宝,并且可以用多种方式创建 REST API。你可以使用纯 Flask 并输出适当的格式(即,JSON 而不是 HTML),或者使用一些开发出来的扩展,使创建 REST API 尽可能简单。这两种解决方案在本质上都是同步的,尽管似乎有积极的发展方向,旨在启用异步支持。

此外,还有一些非常强大和成熟的工具,例如 Tornado,它是一个异步网络库(和服务器),能够扩展到数万个开放连接。最后,在过去的几年里,已经创建了几个基于 Python 的新解决方案。

这些解决方案中的一种,并且可以说是最快的,是 Starlette。被称为轻量级的 ASGI 框架/工具包,它非常适合构建高性能的异步服务。

塞巴斯蒂安·拉米雷斯(Sebastian Ramirez)在 Starlette 和 Pydantic 的基础上构建了 FastAPI,同时通过使用最新的 Python 特性(如类型提示和异步支持)添加了众多功能和优点。根据一些最近的开发者调查 1,FastAPI 正迅速成为最受欢迎和最受欢迎的 Web 框架之一。

1 www.jetbrains.com/lp/devecosystem-2023/python/#python_web_libs_two_years

在本书的后续章节中,您将了解 FastAPI 最重要的功能,但在此阶段,我们将强调拥有一个真正异步的 Python 框架作为系统最多样化组件的粘合剂的重要性。实际上,除了执行通常的 Web 框架任务,如与数据库通信、向前端输出数据、管理身份验证和授权之外,这个 Python 管道还使您能够通过依赖注入系统快速集成并轻松执行频繁需要的任务,如后台作业、头部和正文操作、响应和请求验证等。

本书将尝试涵盖您构建简单 FastAPI 系统所需的绝对最小必要条件,但在过程中,它将考虑各种网络服务器解决方案和部署选项(如 Deta、Heroku 和 DigitalOcean)为您基于 FastAPI 的 Python 后端,同时尝试选择免费解决方案。

因此,简而言之,您应该考虑选择 FastAPI,因为您理想上希望拥有异步处理请求的能力和速度,就像使用 Node.js 服务器一样,同时又能访问 Python 生态系统。此外,您还希望拥有一个框架的简单性和开发速度,该框架可以自动为您生成文档。

在审查了后端组件之后,现在是时候确定你的技术栈并着手前端工作了。下一节将为您简要介绍 React,并讨论它与其他(同样有效)解决方案的区别。

前端——React

当谈到前端——即面向用户的网站部分时,网络世界的变化最为明显。蒂姆·伯纳斯-李(Tim Berners-Lee)于 1991 年首次公开了第一个 HTML 规范,它由文本和不到 20 个标签组成。1994 年,CSS 被引入,网络开始看起来更加美观。传说中,名为 Mocha 的新浏览器脚本语言在短短 10 天内被创造出来——那是在 1995 年。后来,这种语言经历了多次变化,成为了我们今天所熟知的 JavaScript——一种强大且快速的编程语言,随着 Node.js 的出现,它也能够征服服务器。

2013 年 5 月,React 在美国推出,整个 Web 开发界得以见证虚拟 DOM、单向数据流、Flux 模式等。

这只是一点历史,旨在尝试提供一些背景和连贯性,因为 Web 开发,就像任何其他创造性的人类活动一样,很少会跳跃式发展。通常,它是通过一系列步骤发展的,使用户能够解决他们面临的问题。不提 Vue.js 就是不公平的,它是一个构建前端的好选择,同时也拥有一个完整的库生态系统,而 Svelte.js 则在构建 UI 方面提供了一个根本性的转变,即 UI 是编译的,捆绑的大小显著减小。

为什么使用 React?

对于任何面向公众的 Web 应用程序来说,交互式、吸引人、快速且直观的 UI 是必需的。虽然非常困难,但仅使用纯 JavaScript 就可以实现大多数甚至所有简单 Web 应用程序预期提供的功能。FastAPI 能够使用任何兼容的模板引擎(在 Python 世界中,最广泛使用的大概是 Jinja2)来服务 HTML(以及静态文件,如 JavaScript 或 CSS),但我们和用户想要的更多。

与其他框架相比,React 较小。它甚至不被视为框架,而是一个库——实际上,是几个库。尽管如此,它是一个经过超过 10 年开发、为 Facebook 的需求而创建、并由像 Uber、X(前身为 Twitter)和 Airbnb 这样的最大公司使用的成熟产品。

本书没有深入探讨 React,因为我们想专注于 FARM 栈的所有不同部分是如何连接并融入更大图景中的。此外,81% 的开发者已经使用 React2 并且熟悉其功能,因此我们假设我们的读者已经对这一框架有一定程度的了解。

2 2022.stateofjs.com/en-US/libraries/front-end-frameworks/

大多数开发者希望有一个简化和结构化的方式来构建 UI。React 通过依赖 JSX——JavaScript 和 XML 的混合体,具有直观的基于标签的语法,并为开发者提供了一种将应用程序视为组件的方式,这些组件进而形成其他更复杂的组件,从而将构建复杂 UI 和交互的过程分解为更小、更易于管理的步骤,使开发者能够以更简单的方式创建动态应用程序。

使用 React 作为前端解决方案的主要好处可以总结如下:

  • 性能:通过使用在内存中运行的 React 虚拟 DOM,React 应用提供了平滑且快速的性能。

  • 可复用性:由于该应用是通过使用具有自身属性和逻辑的组件构建的,因此您可以一次性编写组件,然后根据需要多次复用它们,从而减少开发时间和复杂性。

  • 易用性:这始终有点主观,但 React 入门很容易。高级概念和模式需要一定程度的熟练度,但即使是新手开发者也能从将应用程序前端拆分为组件并像乐高积木一样使用它们的可能性中立即获得好处。

React 和基于 React 的框架使你作为开发者能够创建具有桌面级外观和感觉的单页应用程序,同时还有对搜索引擎优化有益的服务器端渲染。了解 React 的使用方法使你能够从今天最强大的前端 Web 框架中受益,例如 Next.js、静态站点生成器(如 Gatsby.js)或令人兴奋且充满希望的新来者(如 React Remix)。

版本 16.8中,React 库引入了钩子,使开发者能够在不使用类的情况下使用和操作组件的状态,以及一些 React 的其他功能。这是一个重大的变化,成功地解决了不同的问题——它使得在组件之间重用状态逻辑成为可能,并简化了复杂组件的理解和管理。

最简单的 React 钩子可能是useState钩子。这个钩子使你能够在组件的生命周期内拥有并维护一个状态值(如对象、数组或变量),而无需求助于老式的基于类的组件。

例如,一个可能用于用户尝试找到合适的汽车时过滤搜索结果的非常简单的组件可能包含所需的品牌、型号和生产年份范围。这种功能非常适合作为单独的组件——一个需要维护不同输入控件状态(可能实现为一系列下拉菜单)的搜索组件。让我们看看这种实现的 simplest 可能版本。

以下代码块创建了一个简单的函数组件,它具有单个状态字符串值——一个 HTML select元素,它将更新名为brand的状态变量:

import { useState } from “react”;
const Search = () => {
const [brand, setBrand] = useState(“”);
return (
<div>
<div>Selected brand: {brand}</div>
<select onChange={(ev) => setBrand(ev.target.value)}>
<option value=””>All brands</option>
<option value=”Fiat”>Fiat</option>
<option value=”Ford”>Ford</option>
<option value=”Renault”>Renault</option>
<option value=”Opel”>Opel</option>
</select>
</div>
);
};
export default Search;

粗体行是钩子魔法发生的地方,它必须位于函数体内部。该语句仅仅创建了一个新的状态变量,称为brand,并为你提供了一个 setter 函数,可以在组件内部使用它来设置所需的值。

有许多钩子可以解决不同的问题,本书将介绍以下基本钩子:

  • 声明式视图:在 React 中,你不必担心 DOM 的过渡或突变。React 处理一切,你唯一需要做的就是声明视图的外观和反应。

  • 无模板语言:React 实际上使用 JavaScript 作为模板语言(通过 JSX),因此为了能够有效地使用它,你只需要了解一些 JavaScript,例如数组操作和迭代。

  • 丰富的生态系统:有众多优秀的库可以补充 React 的基本功能——从路由器到自定义钩子、外部库集成、CSS 框架适配等等。

最终,钩子为 React 提供了一种新的方式,在组件之间添加和共享状态逻辑,甚至可以在简单情况下取代 Redux 或其他外部状态管理库的需求。本书中展示的大部分示例都使用了上下文 API——这是一个 React 特性,它允许在不通过不需要它的组件传递 props 的情况下将对象和函数传递到组件树中。结合钩子——useContext钩子——它提供了一种简单直接的方式,在应用的每个部分传递和维护状态值。

React 使用(尽管不是强制性的)功能 JavaScript 的最新特性,ES6 和 ES7,尤其是在数组方面。使用 React 可以提高对 JavaScript 的理解,类似的情况也可以说关于 FastAPI 和现代 Python。

最后一部分将是选择 CSS 库或框架。截至 2024 年,有数十个 CSS 库与 React 兼容,包括 Bootstrap、Material UI、Bulma 等等。许多这些库与 React 合并,成为预构建的自定义和参数化组件的有意义框架。我们将使用 Tailwind CSS,因为它易于设置——并且一旦你掌握了它,它就非常直观。

将 React 部分保持到最基本,应该能让你更多地关注故事中的真正主角——FastAPI 和 MongoDB。如果你愿意,可以轻松地替换 React,无论是 Svelte.js、Vue.js 还是纯手工打造的 ECMAScript。然而,通过学习 React(及其钩子)的基础知识,你将踏上一次美妙的网络开发之旅,这将使你能够使用和理解建立在 React 之上的许多工具和框架。

争议性地,Next.js 是功能最丰富的服务器端渲染 React 框架,它支持快速开发、基于文件系统的路由等等。

摘要

本章为 FARM 堆栈奠定了基础,从描述每个组件的角色到它们的优点。现在,你将自信地选择 FARM 堆栈,并且知道如何在灵活和流动的网络开发项目中实现它。既然你在阅读,我会假设我的案例是有说服力的——你对它仍然感兴趣,并准备好探索 FARM 堆栈。

下一章将提供一个快速、简洁、可操作的 MongoDB 概述,然后为你的 FARM 应用程序设置数据存储层。随着你的进展,我们相信你会发现 FastAPI、React 和 MongoDB 的组合是你下一个网络应用程序的最佳选择。

第二章:使用 MongoDB 设置数据库

在本章中,您将通过几个简单而具有说明性的示例来探索 MongoDB 的一些主要功能。您将了解 MongoDB 查询 API 的基本命令,以开始与存储在 MongoDB 数据库中的数据进行交互。您将学习到必要的命令和方法,使您能够插入、管理、查询和更新您的数据。

本章的目的是帮助您了解在本地机器或云上设置 MongoDB 数据库是多么容易,以及如何在快速发展的 Web 开发过程中执行可能需要的操作。

通过 MongoDB 方法和聚合进行查询,最佳的学习方式是通过实验数据。本章利用 MongoDB Atlas 提供的真实世界样本数据集,这些数据集已加载到您的云数据库中。您将学习如何对这些数据集执行 CRUD 和聚合查询。

本章将涵盖以下主题:

  • MongoDB 数据库的结构

  • 安装 MongoDB 社区服务器和工具

  • 创建 Atlas 集群

  • MongoDB 查询和 CRUD 操作

  • 聚合框架

技术要求

对于本章,您需要 MongoDB 版本 7.0.7 和 Windows 11(以及 Ubuntu 22.04 LTS)。

MongoDB 版本 7.0 与以下兼容:

  • Windows 11、Windows Server 2019 或 Windows Server 2022(64 位版本)

  • Ubuntu 20.04 LTS(Focal)和 Ubuntu 22.04 LTS(Jammy)Linux(64 位版本)

以下是一些推荐的系统配置:

  • 至少配备 8 GB RAM 的台式机或笔记本电脑。

  • 没有指定 CPU 要求,但请确保它是现代的(多核处理器),以确保高效性能。

MongoDB 数据库的结构

MongoDB 在流行度和使用方面被广泛认为是领先的 NoSQL 数据库——其强大的功能、易用性和多功能性使其成为大型和小型项目的绝佳选择。其可扩展性和性能使得您的应用程序的数据层拥有非常坚实的基础。

在以下章节中,您将更深入地了解 MongoDB 的基本概念和构建块:文档、集合和数据库。由于本书采用自下而上的方法,您将从最底层开始,了解 MongoDB 中可用的最简单数据结构的概述,然后从这里开始,进入文档、集合等。

文档

MongoDB 是一个面向文档的数据库。但这实际上意味着什么呢?

在 MongoDB 中,文档类似于传统关系数据库中的行。MongoDB 中的每个文档都是一个由键值对组成的数据结构,代表一条记录。存储在 MongoDB 中的数据为应用程序开发者提供了极大的灵活性,使他们能够根据需要建模数据,并允许他们随着应用程序需求的变化在未来轻松地演进模式。MongoDB 具有灵活的模式模型,这基本上意味着你可以在集合中的不同文档中拥有不同的字段。根据需要,你还可以为文档中的字段使用不同的数据类型。

然而,如果你的应用程序需要在集合中的文档中保持数据的一致结构,你可以使用 MongoDB 中的模式验证规则来强制一致性。MongoDB 使你能够以对应用程序需求最有意义的方式存储数据。

MongoDB 中的文档只是一个有序的键值对集合。在这本书中,术语字段可以互换使用,因为它们代表同一事物。这种结构,正如你稍后将要探索的,与每种编程语言中的数据结构相对应;在 Python 中,你会发现这种结构是一个字典,非常适合 Web 应用程序或桌面应用程序的数据流。

创建文档的规则相当简单:键/字段名称必须是字符串,有一些例外,你可以在文档中了解更多信息,并且一个文档不能包含重复的键名。请记住,MongoDB 是区分大小写的。

在本章中,你将把一个名为sample_mflix的示例数据集加载到你的 MongoDB Atlas 集群中。该数据集包含许多集合,但本章中对我们感兴趣的是movies集合,它包含描述电影的文档。以下文档可能存在于这个集合中:

{
  _id: ObjectId("573a1390f29313caabcd42e8"),
  plot: 'A group of bandits stage a brazen train hold-up, only to find a determined posse hot on their heels.',
  genres: [ 'Short', 'Western' ],
  runtime: 11,
  cast: [
    'A.C. Abadie',
    "Gilbert M. 'Broncho Billy' Anderson",
    'George Barnes',
    'Justus D. Barnes'
  ],
  poster: 'https://m.media-amazon.com/images/M/MV5BMTU3NjE5NzYtYTYyNS00MDVmL WIwYjgtMmYwYWIxZDYyNzU2XkEyXkFqcGdeQXVyNzQzNzQxNzI@._V1_SY1000_SX677_AL_.jpg',
  title: 'The Great Train Robbery',
  fullplot: "Among the earliest existing films in American cinema - notable as the first film that presented a narrative story to tell - it depicts a group of cowboy outlaws who hold up a train and rob the passengers. They are then pursued by a Sheriff's posse. Several scenes have color included - all hand tinted.",
  languages: [ 'English' ],
  released: ISODate("1903-12-01T00:00:00.000Z"),
  directors: [ 'Edwin S. Porter' ],
  rated: 'TV-G',
  awards: { wins: 1, nominations: 0, text: '1 win.' },
  lastupdated: '2015-08-13 00:27:59.177000000',
  year: 1903,
  imdb: { rating: 7.4, votes: 9847, id: 439 },
  countries: [ 'USA' ],
  type: 'movie',
  tomatoes: {
    viewer: { rating: 3.7, numReviews: 2559, meter: 75 },
    fresh: 6,
    critic: { rating: 7.6, numReviews: 6, meter: 100 },
    rotten: 0,
    lastUpdated: ISODate("2015-08-08T19:16:10.000Z")
  },
  num_mflix_comments: 0
}

注意

当涉及到文档中的文档嵌套时,MongoDB 支持 100 层嵌套,这在大多数应用程序中你可能不会达到这个限制。

MongoDB 支持的数据类型

MongoDB 允许你将任何 BSON 数据类型作为字段值存储。BSON 与 JSON 非常相似,它代表“二进制 JSON”。BSON 的二元结构使其更快,并且比 JSON 支持更多的数据类型。在设计任何类型的应用程序时,最重要的决定之一是数据类型的选择。作为一个开发者,你永远不会想为当前的工作使用错误工具。

注意

MongoDB 支持的所有数据类型的完整列表可以在官方文档中找到:www.mongodb.com/docs/mongodb-shell/reference/data-types/

MongoDB 支持的一些最重要的数据类型包括:

  • 字符串:这些可能是 MongoDB 中最基本和最通用的数据类型,它们用于表示文档中的所有文本字段。

  • 数字:MongoDB 支持不同类型的数字,包括:

    • int:32 位整数

    • long:64 位整数

    • double:64 位浮点数

    • decimal:基于 128 位的十进制浮点数

  • truefalse值;它们不使用引号书写,因为你不希望它们被解释为字符串。

  • 对象或内嵌文档:在 MongoDB 中,文档内的字段可以包含内嵌文档,允许在单个文档中进行复杂的数据结构化。这种能力支持类似 JSON 的结构深层嵌套,便于灵活和分层的数据建模。

  • 数组:数组可以包含零个或多个值,具有类似列表的结构。数组的元素可以是任何 MongoDB 数据类型,包括其他文档和数组。它们是从零开始的,特别适合创建内嵌关系。例如,你可以在博客文章文档本身中存储所有评论,包括时间戳和发表评论的用户。数组可以利用标准的 JavaScript 数组方法进行快速编辑、推送和其他操作。

  • _id作为主键。如果插入的文档省略了_id字段,MongoDB 会自动为_id字段生成一个 ObjectId,用于在集合中唯一标识文档。ObjectId 的长度为 12 字节。它们体积小、可能唯一、生成速度快、有序。这些 ObjectId 广泛用作传统关系的键——ObjectId 会自动索引。

  • 日期:尽管 JSON 不支持日期类型并将它们存储为普通字符串,但 MongoDB 的 BSON 格式明确支持日期类型。它们表示自 Unix 纪元(1970 年 1 月 1 日)以来的 64 位毫秒数。所有日期都存储为 UTC,没有时区关联。BSON 日期类型是有符号的。负值表示 1970 年之前的日期。

  • 二进制数据:二进制数据字段可以存储任意二进制数据,是保存非 UTF-8 字符串到数据库的唯一方式。这些字段可以与 MongoDB 的 GridFS 文件系统结合使用,例如存储图像。

  • Null:这可以表示 null 值或不存在的字段,我们甚至可以将 JavaScript 函数作为不同的数据类型存储。

现在你已经了解了 MongoDB 中可用的字段类型以及如何将你的业务逻辑映射到(灵活的)模式中,是时候介绍集合了——文档的组,在关系数据库世界中与表相对应。

集合和数据库

尽管你可以在同一个集合中存储多个模式,但有许多理由将你的数据存储在多个数据库和多个集合中:

  • 数据分离:集合允许你逻辑上分离不同类型的数据。例如,你可以有一个用于用户数据的集合,另一个用于产品数据的集合,还有一个用于订单数据的集合。这种分离使得管理和查询特定类型的数据变得更加容易。

  • 性能优化:通过将数据分离到不同的集合中,你可以通过更有效地索引和查询特定集合来优化性能。这可以提高查询性能并减少需要扫描的数据量。

  • 数据局部性:在集合中将相同类型的文档分组将需要更少的磁盘查找时间,考虑到索引是由集合定义的,查询效率会更高。

虽然单个 MongoDB 实例可以同时托管多个数据库,但将应用程序中使用的所有文档集合都保存在单个数据库中是一种良好的做法。

注意

当你安装 MongoDB 时,将创建三个数据库,它们的名称不能用于你的应用程序数据库:adminlocalconfig。它们是内置数据库,不应被替换,因此请避免意外地将你的数据库命名为相同的方式或对这些数据库进行任何更改。

安装 MongoDB 数据库的选项

在回顾了 MongoDB 数据库的基本术语、概念和结构之后,现在是时候学习如何在本地和云端设置 MongoDB 数据库服务器了。

本地数据库设置方便快速原型设计,甚至不需要互联网连接。然而,我们建议你在设置数据库作为未来章节中用作后端时,使用 MongoDB Atlas 提供的云托管数据库。

MongoDB Atlas 相比本地安装提供了许多优势。首先,它易于设置,正如你将看到的,你可以在几分钟内将其设置好并运行起来,一个慷慨的免费层数据库已经准备好工作。MongoDB 处理数据库的所有操作方面,如配置、扩展、备份和监控。

Atlas 取代了大部分手动设置并保证了可用性。其他好处包括 MongoDB 团队的参与(他们试图实施最佳实践),默认情况下具有访问控制、防火墙和细粒度访问控制、自动备份(取决于层级),以及立即开始生产力的可能性。

安装 MongoDB 和相关工具

MongoDB 不仅仅是一个数据库服务提供商,而是一个完整的开发者数据平台,它围绕核心数据库构建了一系列技术,以满足你所有的数据需求并提高你的开发效率。让我们检查以下组件,你将在接下来的章节中安装或使用它们:

  • MongoDB 社区版:MongoDB 的免费版本,可在所有主要操作系统上运行。这是你将在本地玩转数据时使用的版本。

  • MongoDB Compass:一个用于在可视化环境中管理、查询、聚合和分析 MongoDB 数据的图形用户界面(GUI)。Compass 是一个成熟且实用的工具,您将在初始查询和聚合探索过程中使用它。

  • MongoDB Atlas:MongoDB 的数据库即服务解决方案。这一服务是 MongoDB 成为 FARM 堆栈核心部分的主要原因之一。它相对容易设置,并且可以减轻您手动管理数据库的负担。

  • (mongosh):一个命令行外壳,不仅可以在您的数据库上执行简单的创建读取更新删除CRUD)操作,还允许执行管理任务,如创建和删除数据库、启动和停止服务以及类似的工作。

  • MongoDB 数据库工具:几个命令行实用程序,允许管理员和开发者将数据从数据库导出或导入,提供诊断功能,或允许操作存储在 MongoDB 的 GridFS 系统中的大文件。

本章将重点介绍实现完全功能安装的流程。请检查与您的操作系统对应的安装说明。本章包括 Windows、Linux 和 macOS 的安装说明。

在 Windows 上安装 MongoDB 和 Compass

在本节中,您将学习如何安装 MongoDB 社区版最新版本,撰写本文时为 7.0。MongoDB 社区版仅支持 x86_64 架构的 64 位 Windows 版本。支持的 Windows 版本包括 Windows 11、Windows Server 2019 和 Windows Server 2022。要安装 MongoDB 和 Compass,您可以参考以下步骤。

注意

我们强烈建议您检查 MongoDB 网站上的说明(www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/),以确保您能够获取最新信息,因为它们可能会有所变化。

  1. 要下载安装程序,请访问 MongoDB 下载中心www.mongodb.com/try/download/community,选择 Windows 版本,然后点击下载,如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_01.jpg

图 2.1:MongoDB 下载页面

  1. 接下来,执行它。如果出现安全提示“打开可执行文件”,请选择,然后继续进入 MongoDB 设置向导。

  2. 阅读许可协议,勾选复选框,然后点击下一步

  3. 这是一个重要的屏幕。当被问及选择哪种设置类型时,请选择完整,如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_02.jpg

图 2.2:完整安装

  1. 下一个向导将询问您是否希望 MongoDB 作为 Windows 网络服务(您应该选择这种方式)或本地和域服务运行。保留默认值,不要做任何更改,直接进入下一步。

  2. 另一个向导将出现,提示你是否想安装 Compass,MongoDB 的数据库管理 GUI 工具。选择复选框并继续安装:

图 2.3:安装 Compass

  1. 最后,Windows 的 用户账户控制UAC)警告屏幕将弹出,你应该选择

现在你已经在本地机器上安装了 MongoDB Community Server,下一节将展示如何安装你将在本书中使用的其他必要工具。

安装 MongoDB Shell (mongosh)

在您的计算机上安装 MongoDB Community Server 和 Compass 后,接下来将安装 mongosh,MongoDB Shell。

注意

关于其他操作系统的说明,请访问 MongoDB 文档:www.mongodb.com/docs/mongodb-shell/install/.

这里是如何在 Windows 上操作的:

  1. 导航到 MongoDB 下载中心 (www.mongodb.com/try/download/shell),在 工具 部分选择 MongoDB Shell

  2. 从下拉菜单中选择 Windows 版本和 msi 包,然后点击 下载

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_04.jpg

图 2.4:下载 MongoDB Shell

  1. 接下来,在您的计算机上找到 msi 包并执行它。如果安全提示要求 打开可执行文件,选择 并继续到 MongoDB 设置向导。向导将打开以下页面。点击 下一步

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_05.jpg

图 2.5:MongoDB Shell 设置向导

  1. 在提示中,选择安装 mongosh 的目标文件夹,或者如果你觉得默认选项看起来不错,就保持默认,然后完成安装。

  2. 到目前为止,你应该能够测试 MongoDB 是否正在运行(作为服务)。在你的选择命令提示符中输入以下命令(最好是使用 cmder,可在 cmder.app 获取):

    mongosh
    
  3. 你应该会看到各种通知和一个标记为 > 的小提示。尝试输入以下内容:

    Show dbs
    

    如果你看到自动生成的 adminconfiglocal 数据库,你应该可以继续了。

  4. 现在,检查 Compass 的安装情况。在 Windows 上,你应该能在开始菜单下的 MongoDBCompass(无空格)中找到它。

  5. 如果你只是点击 27017,你应该能看到当你使用 MongoDB 命令行时看到的全部数据库:adminconfiglocal

MongoDB 数据库工具

MongoDB 数据库工具是一组用于与 MongoDB 部署一起使用的命令行实用程序。以下是一些常见的数据库工具:

  • mongoimport:从扩展的 JSON、CSV 或 TSV 导出文件导入内容

  • mongoexport:从 mongod 实例中生成 JSON 或 CSV 导出

  • mongodump:创建 mongod 数据库内容的二进制导出

有一些其他工具,例如 mongorestorebsondumpmongostatmongotopmongofiles。MongoDB 数据库工具可以使用 MSI 安装程序安装(或作为 ZIP 存档下载)。

注意

msi 软件包可以从 MongoDB 下载中心下载(www.mongodb.com/try/download/database-tools)。

下载后,您可以按照 MongoDB 文档中提供的安装说明进行操作(www.mongodb.com/docs/database-tools/installation/installation-windows/)。

下一节将介绍在标准 Linux 发行版上安装 MongoDB 的过程。

在 Linux 上安装 MongoDB 和 Compass:Ubuntu

Linux 为本地服务器的开发和管理工作提供了许多好处,但最重要的是,如果您决定不再使用 MongoDB 的数据库即服务,您可能希望在一个基于 Linux 的服务器上工作。

在本书中,我们将介绍在 Ubuntu 22.04 LTS(Jammy)版本上的安装过程,同时 MongoDB 版本也支持 x86_64 架构的 Ubuntu 20.04 LTS(Focal)。安装 MongoDB Ubuntu 所需的步骤将在此列出,但您应始终检查 MongoDB Ubuntu 安装页面(www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/)以了解最近的变化。然而,过程本身不应发生变化。

以下操作需要在 Bash shell 中执行。下载允许您安装 MongoDB 的公钥,然后您将创建一个列表文件并重新加载包管理器。对于其他 Linux 发行版,也需要执行类似的步骤,因此请确保检查您选择发行版的网站上的文档。最后,您将通过包管理器执行 MongoDB 的实际安装并启动服务。

总是最好跳过 Linux 发行版提供的软件包,因为它们通常没有更新到最新版本。按照以下步骤在 Ubuntu 上安装 MongoDB:

  1. 按照以下方式导入软件包管理系统中使用的公钥。

    您的系统上需要安装 gnupgcurl。如果您还没有安装它们,可以通过运行以下命令进行安装:

    sudo apt-get install gnupg curl
    

    要导入 MongoDB 公共 GPG 密钥,请运行以下命令:

    curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
       sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg \
       --dearmor
    

    通过运行以下命令为 Ubuntu 22.04(Jammy)创建 /etc/apt/sources.list.d/mongodb-org-7.0.list 文件:

    echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
    
  2. 使用以下命令重新加载本地包数据库:

    sudo apt-get update
    
  3. 安装 MongoDB 软件包。要安装最新稳定版本,请执行以下命令:

    sudo apt-get install -y mongodb-org
    
  4. 运行 MongoDB 社区版。如果您遵循这些说明并通过包管理器安装 MongoDB,则在安装过程中将创建/var/lib/mongodb数据目录和/var/log/mongodb日志目录。

  5. 您可以使用以下命令启动mongod进程:

    sudo systemctl start mongod
    Failed to start mongod.service: Unit mongod.service not found.
    

    首先运行以下命令:

    sudo systemctl daemon-reload
    

    然后再次运行start命令(如步骤 5所示)。

  6. 您只需简单地输入以下命令即可开始使用 MongoDB Shell (mongosh):

    mongosh
    

当涉及到安装和进程管理时,MongoDB 与其他 Linux 软件并没有特别不同。然而,如果您在安装过程中遇到任何问题,第一个建议是访问 MongoDB Linux 安装页面。

设置 Atlas

MongoDB Atlas——由 MongoDB 提供的云数据库服务——是 MongoDB 最强大的卖点之一。

MongoDB Atlas 是一项完全托管的数据服务,这意味着 MongoDB 为您处理基础设施管理、数据库设置、配置和维护任务。这使您能够专注于开发应用程序,而不是管理底层基础设施。

注意

www.mongodb.com/docs/atlas/getting-started/上详细记录了注册和设置 MongoDB Atlas 实例的过程。

您可以通过两种方式设置您的 Atlas 账户:

  • Atlas UI(网站)

  • Atlas CLI(命令行)

Atlas CLI 为 MongoDB Atlas 提供了一个专门的命令行界面,允许您从终端直接管理您的 Atlas 数据库部署和 Atlas Search。在本书中,您将看到如何从 UI 进行操作。

如果您还没有 Atlas 账户,请前往www.mongodb.com/cloud/atlas/register创建一个 Atlas 账户。您可以使用 Google 账户、GitHub 账户或电子邮件账户注册此服务。

注意

随着更多功能的引入,Atlas UI 和集群创建步骤可能会发生变化。强烈建议您在设置集群时参考最新说明(www.mongodb.com/docs/atlas/getting-started/)。

在设置账户(这里使用的是 Gmail 地址,因此您可以使用 Google 账户登录以实现更快的访问)之后,系统会提示您创建一个集群。您将创建一个免费的M0集群,并且应该选择一个尽可能接近您物理位置的云提供商和区域选项,以最小化延迟。

创建 Atlas 集群

要设置一个 Atlas 集群,请执行以下步骤:

  1. 在您的 Atlas 仪表板上,您将看到创建部署选项。点击创建以开始创建您的第一个 Atlas 集群的过程。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_06.jpg

图 2.6:Atlas 仪表板

在这一步,您需要做几件事情:

  1. 选择免费的M0 沙盒选项。

  2. 给您的集群起一个有意义的名字,例如farm-stack。您可以选择任何其他名字。

  3. 确保已勾选自动安全设置添加示例数据集选项。这将在以后非常有用。

  4. 选择您偏好的云服务提供商(默认为 AWS)

  5. 选择离您位置最近的区域以最小化延迟,然后点击创建部署

注意

创建 Atlas 用户和设置 IP 是一个重要的步骤,您必须在开始使用 Atlas 集群之前完成。

  1. 在下一屏,您将被要求创建一个数据库用户,该用户将有一个用户名和密码。这两个字段都是自动填充的,以简化流程。您可以根据自己的偏好更改用户名和密码。请确保将密码保存在某个地方,因为您稍后连接到 您的集群时需要它**。

  2. 默认情况下,您的当前 IP 地址被添加以启用本地连接。MongoDB Atlas 提供了许多安全层,受限 IP 地址访问是其中之一。如果您打算从任何其他 IP 地址使用您的集群,您可以稍后添加该 IP,或者您也有选择启用从任何地方访问(0.0.0.0/0),这将允许您从任何地方连接,但出于安全原因,这不是推荐选项。

完成这些步骤后,您已成功创建了第一个 Atlas 集群!

获取 Atlas 集群的连接字符串

接下来,您将查看为您自动加载的示例数据集。在本节中,您将使用 Compass 将数据集连接到您的 Atlas 集群,并使用它来探索相同的数据集:

  1. 在 Atlas 仪表板上,点击如图图 2.7所示的浏览集合按钮。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_07.jpg

图 2.7:Atlas 仪表板

  1. 您可以看到sample_mflix数据集已经加载到您的集群中。您将拥有一个名为sample_mflix的数据库,并在其下创建六个集合:commentsembedded_moviesmoviessessionstheatresusers

  2. 现在,前往您的 Atlas 仪表板,获取从 Compass 连接到 Atlas 集群的连接字符串。

  3. 在 Atlas 仪表板上,点击绿色的连接按钮。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_08.jpg

图 2.8:连接到您的集群

  1. 然后,选择Compass

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_09.jpg

图 2.9:连接到您的集群

  1. 在下一个向导中,复制框中显示的连接字符串:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_10.jpg

图 2.10:获取连接字符串

太好了!现在,您已经有了 Atlas 集群的连接字符串。您可以去 Compass 并使用此连接字符串从 Compass 连接到您的 Atlas 集群。连接到集群之前,别忘了将<password>替换为您的 Atlas 用户密码。

从 Compass 连接到 Atlas 集群

执行以下步骤以从 Compass 连接到您的 Atlas 集群:

  1. 如果 Compass 尚未在您的计算机上运行,请启动它。在URI框中,粘贴您从上一步复制的连接字符串,并添加您的密码。接下来,单击连接

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_11.jpg

图 2.11:MongoDB Compass

  1. 成功连接到您的 Atlas 集群后,您将看到类似于图 2.12的内容:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_12.jpg

图 2.12:MongoDB Compass 中的“我的查询”选项卡

  1. 您可以在左侧面板中看到您集群中的数据库列表。单击sample_mflix以展开下拉菜单并显示集合列表。然后,单击movies以查看该集合中存储的文档:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_13.jpg

图 2.13:集合中的文档列表

图 2.13 显示,你的sample_mflix.movies集合中有 21.4k 个文档。

现在,您应该在您的机器上拥有一个功能齐全的全球最受欢迎的 NoSQL 数据库实例。您还创建了一个在线账户,并成功创建了您自己的集群,准备好应对大多数数据挑战并为您的 Web 应用提供动力。

MongoDB 查询和 CRUD 操作

让我们看看 MongoDB 的实际应用,亲身体验全球最受欢迎的 NoSQL 数据库的力量。本节将通过简单的示例向您展示最关键的 MongoDB 命令。这些方法将使您作为开发者能够控制您的数据,创建新的文档,使用不同的标准和条件查询文档,执行简单和更复杂的聚合,并以各种形式输出数据。

虽然您将通过 Python 驱动程序(Motor 和 PyMongo)与 MongoDB 进行通信,但首先学习如何直接编写查询是有帮助的。您将从查询在集群创建时导入的sample_mflix.movies数据集开始,然后您将经历创建新数据的过程——插入、更新等。

让我们先定义执行 MongoDB 命令的两种选项,如下所示:

  • Compass 图形用户界面

  • MongoDB Shell (mongosh)

mongosh连接到您的 MongoDB Atlas 集群并执行数据上的 CRUD 操作:

  1. 要从mongosh(MongoDB Shell)连接到您的 Atlas 集群,请导航到您的 Atlas 集群仪表板并获取mongosh的连接字符串。步骤将与 Compass 相同,只是连接工具不同。为此,您需要 MongoDB Shell 而不是 Compass。

    图 2.14 显示了mongosh的连接字符串:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_14.jpg

图 2.14:连接到 mongosh(MongoDB Shell)

  1. 复制连接字符串并导航到您计算机上的 CLI。

  2. 现在,为了在 Atlas 中的云数据库上设置与执行命令的选项,请执行以下步骤:

    1. 在 shell 会话(Windows 上的命令提示符或 Linux 上的 Bash)中,将连接字符串粘贴到提示符中,然后按Enter键。然后,输入密码并按Enter键。

    您也可以通过使用--password选项后跟您的密码来显式地在连接字符串中传递密码。为了避免在输入密码时出现任何拼写错误或错误,您可以使用此选项。

    1. 成功连接到您的 Atlas 集群后,您应该看到如下内容:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_15.jpg

图 2.15:成功连接到 MongoDB 数据库

  1. 接下来,使用show dbs命令列出集群中所有存在的数据库:
show dbs

此命令应列出所有可用的数据库:adminlocalsample_mflix(您的数据库)。

  1. 为了使用您的数据库,请输入以下代码:
use sample_mflix

控制台将响应switched to db sample_mflix,这意味着现在您可以查询并操作您的数据库。

  1. 要查看sample_mflix中的可用集合,请尝试以下代码:
show collections

您应该能够查看在 Atlas UI 和 Compass 中看到的六个集合,即commentsembedded_moviesmoviessessionstheatresusers。现在您已经有了可用的数据库和集合,您可以继续使用一些查询选项。

MongoDB 中的查询

本节将通过使用sample_mflix.movies集合作为示例来展示find()的使用。使用具有预期查询结果的真实数据有助于巩固所学知识,并使理解底层过程更加容易和全面。

本章将涵盖的最常见的 MongoDB 查询语言命令如下:

  • find(): 根据简单或复杂的标准查找和选择文档

  • insertOne(): 将新文档插入到集合中

  • insertMany(): 将一个文档数组插入到集合中

  • updateOne()updateMany(): 根据某些标准更新一个或多个文档

  • deleteOne()deleteMany(): 从集合中删除一个或多个文档

sample_mflix.movies集合中有 21,349 个文档。要查询所有文档,请在 MongoDB Shell 中输入以下命令:

db.movies.find()

上述命令将打印出几个文档,如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_16.jpg

图 2.16:find()查询输出

控制台将打印出消息“输入“it”以获取更多信息”,因为控制台一次只打印出 20 个文档。这个语句在 SQL 世界中可以理解为经典的SELECT * FROM TABLE

注意

find()方法返回一个游标而不是实际的结果。游标允许对返回的文档执行一些标准数据库操作,例如限制结果数量、按一个或多个键(升序或降序)排序以及跳过记录。

您还可以应用一些过滤器,只返回满足指定条件的文档。movies集合有一个years字段,它表示电影发布的年份。例如,您可以编写一个查询来只返回在1969年发布的电影。

在命令提示符中,输入以下命令:

db.movies.find({"year": 1969}).limit(5)

在这里,你使用了游标上的 limit() 方法来指定游标应返回的最大文档数,在这种情况下为 5

上述命令将返回搜索结果:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/flstk-fapi-rct-mongo/img/B22406_02_17.jpg

图 2.17:带有过滤条件的 find() 操作

结果现在应仅包含满足 year 键等于 1969 的条件的文档。查看结果,似乎有很多文档。你还可以通过使用 db.collection.countDocuments() 方法在查询上执行计数操作。例如:

db.movies.countDocuments({"year": 1969})

上述命令返回 107,这意味着你的集合中有 107 个文档符合你的搜索条件;也就是说,有 107 部电影是在 1969 年发布的。

你在之前的查询中使用的 JSON 语法是一个 过滤器,它可以有多个键值对,用于定义你的查询方法。MongoDB 有许多操作符,允许你查询具有比简单相等更复杂条件的字段,并且它们的最新文档可在 MongoDB 网站上找到,网址为 docs.mongodb.com/manual/reference/operator/query/

你可以访问该页面并查看一些操作符,因为它们可以给你一个关于如何构建你的查询的想法。

例如,假设你想找到所有在 USA 发布且年份在 1945 之后的 Comedy (类型) 电影。以下查询将完成这项工作:

db.movies.find({"year": {$gt: 1945}, "countries": "USA", "genres": "Comedy"})

执行查询后,你应该会看到游标返回的一堆文档。

你也可以使用 countDocuments 方法来找出匹配过滤条件的文档的确切数量:

db.movies.countDocuments({"year": {$gt: 1945}, "countries": "USA", "genres": "Comedy"})

你会发现集合中有 3521 个文档符合你的搜索条件。

$gt 操作符用于指定年份应大于 1945,确保所选电影是在此年之后发布的。国家和类型的条件很简单,需要 countries 数组包含 USA,而 genres 数组包含 Comedy

记住,find() 方法意味着一个 AND 操作,因此只有满足所有三个条件的文档才会被返回。

一些最广泛使用的查询操作符如下:

  • $gt:大于

  • $lt:小于

  • $in:提供值列表

然而,你可以在 MongoDB 文档中看到更多——逻辑上的 ;用于在地图上查找最近点的 地理空间 操作符等等。现在是时候探索其他允许你执行查询和操作的方法了。

findOne()find() 类似;它也接受一个可选的过滤参数,但只返回满足条件的第一个文档。

在你深入研究创建、删除和更新现有文档的过程之前,重要的是要提到一个非常有用的功能,称为投影

投影

投影允许你指定在查询结果中应包含或排除哪些字段。这是通过向find()findOne()方法提供额外的参数来实现的。此参数是一个对象,它指定了要包含或排除的字段,从而有效地定制查询结果,只包含所需的信息。

构建投影很简单;一个投影查询只是一个 JSON 对象,其中键是字段的名称,而值是0(如果你想从输出中排除一个字段)或1(如果你想包含它)。ObjectId类型默认包含,所以如果你想从输出中移除它,你必须明确将其设置为0。此外,如果你没有在投影中包含任何字段的名称,它假定具有0值,并且不会被投影。

假设在你之前的查询中,你只想投影电影标题、上映国家和年份。为此,执行以下命令:

db.movies.find({"year": {$gt: 1945}, "countries": "USA", "genres": "Comedy"}, {"_id":0, "title": 1, "countries": 1, "year": 1}).sort({"year": 1}).limit(5)

排序和限制操作首先按year字段升序排序返回的文档,然后根据limit参数限制结果为五份文档。在投影部分,通过将其设置为0来抑制_id字段,并通过将其设置为1来包含titlecountriesyear字段。由于在投影中省略了genres字段和所有其他字段,它们自动被排除在返回的文档之外。

创建新文档

在 MongoDB 中创建新文档的方法是insertOne()。你可以尝试将以下虚构电影插入到你的数据库中:

db.movies.insertOne({"title": "Once upon a time on Moon", "genres":["Test"], year: 2024})

上述命令将打印以下消息:

{
  acknowledged: true,
  insertedId: ObjectId("66b25f48b959c3fb3a4e56ed")
}

第一部分表示 MongoDB 已确认插入操作,而第二部分则打印出ObjectId键,这是 MongoDB 使用的并自动分配给新插入文档的主键,除非手动提供。

自然地,MongoDB 也支持使用insertMany()方法一次性插入多个文档。该方法接受一个文档数组,而不是单个文档。例如,你可以按如下方式插入另外几部样本电影:

db.movies.insertMany([{"title": "Once upon a time on Moon", "genres":["Test"], year: 2024}, {"title": "Once upon a time on Mars", "genres":["Test"], year: 2023}, {"title": "Tiger Force in Paradise", "genres":["Test"], year: 2019, rating: "G"}])

在这里,你插入了三部虚构的电影,第三部有一个新的属性,评分(设置为G),这在任何其他电影中都不存在,只是为了突出 MongoDB 的架构灵活性。Shell 也确认了这一点,并打印出新插入文档的ObjectId键。

更新文档

在 MongoDB 中更新文档可以通过几种不同的方法来实现,这些方法适合于可能出现在你的业务逻辑中的不同场景。

updateOne() 方法使用在字段中提供的数据更新遇到的第一个文档。例如,让我们更新第一个 genres 字段包含 Test 的电影,并将其设置为 PlaceHolder 类型,如下所示:

db.movies.updateOne({genres: "Test"}, {$set: {"genres.$": "PlaceHolder"}})

只要使用 $set 操作符,你也可以更新文档的现有属性。假设你想要更新你收藏中所有匹配过滤条件且 genres 字段值设置为 placeHolder 类型的文档,并将年份值增加 1。你可以尝试以下命令:

db.movies.updateMany( { "genres": "Test" }, { $set: { "genres.$": "PlaceHolder" }, $inc: { "year": 1 } } )

上述命令更新了许多文档,即所有 genres 字段包含 Test 的电影。

更新文档是一个原子操作——如果同时发出两个或多个更新,则首先到达服务器的更新将被应用。

mongosh 还提供了一个 replaceOne() 方法,它接受一个过滤器,就像你之前的方法一样,但还期望一个完整的文档来替换前面的文档。你可以在以下文档中获取有关集合方法的更多信息:www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/

删除文档

删除文档的方式与 find 方法类似——你可以提供一个过滤器来指定要删除的文档,并使用 deleteOnedeleteMany 方法来执行操作。

使用以下命令删除你收藏中插入的所有假电影:

db.movies.deleteMany({genres: "PlaceHolder"})

壳会通过一个 deletedCount 变量来确认此操作,其值等于 4——被删除的文档数量。deleteOne 方法以非常相似的方式通过删除第一个匹配过滤条件的文档来操作。

要在 MongoDB 中删除整个集合,你可以使用 db.collection.drop() 命令。然而,不建议在不加考虑的情况下删除整个集合,因为它将删除所有数据和相关的索引。建议不要为电影数据集运行此命令,因为我们还需要它来完成本章的其余部分。

注意

如果你删除了所有文档,请确保在 Atlas 中再次导入数据(你应在 Atlas 仪表板上看到一个选项)。

聚合框架

MongoDB 聚合框架是一个极其有用的工具,它可以将一些(或大多数)计算和不同复杂度的聚合负担卸载到 MongoDB 服务器,从而减轻你的客户端以及(基于 Python 的)后端的工作量。

围绕一个 find 方法展开,你已经广泛使用了这个方法,但额外的好处是在不同的阶段或步骤中进行数据处理。

如果你想要熟悉所有可能性,MongoDB 文档网站([www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/](https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/))是最佳起点。然而,我们将从几个简单的示例开始。

聚合的语法与其他你之前使用的方法类似,例如 find()findOne()。我们使用 aggregate() 方法,它接受一个阶段列表作为参数。

可能最好的聚合开始方式是模仿 find 方法。

编写一个聚合查询以选择所有 genres 字段包含 Comedy 的电影:

db.movies.aggregate([{$match: {"genres": "Comedy"}}])

这可能是最简单的聚合,它只包含一个阶段,即 $match 阶段,它告诉 MongoDB 你只想获取喜剧电影,因此第一个阶段的输出正好是这些。

在你的集合中,你既有 series 数据也有 movies 数据。让我们编写一个聚合管道来过滤出类型为电影且类型为 Comedy 的电影。然后,将它们分组在一起以找出喜剧电影的平均时长:

db.movies.aggregate([ {$match: {type: "movie", genres: "Comedy" } }, {$group: {_id: null, averageRuntime: { $avg: "$runtime" } } } ])

上述代码将返回以下输出:

[ { _id: null, averageRuntime: 98.86438291881745 } ]

这里是对前面聚合查询的更详细解释:

  • $match 定义了过滤文档的标准。在这种情况下,{type: "movie", genres: "Comedy"} 指定文档必须具有类型等于 movie,并且它们的 genres 数组中必须包含 Comedy 才能通过。

  • $group 阶段接受定义如何分组文档以及要对分组数据执行哪些计算的参数。

  • $group 阶段,_id 指定分组标准。将 _id 设置为 null 意味着所有从前一个阶段传递过来的文档将被聚合到一个单独的组中,而不是根据不同的字段值分成多个组。

  • $avg 是一个累加器运算符,在这里用于计算平均值。$runtime 指定每个文档的运行时字段应用于计算。

一旦数据按照你的要求分组和聚合,你就可以应用其他更简单的操作,例如排序、排序和限制。

摘要

本章详细介绍了定义 MongoDB 及其结构的基石。你已经看到了如何使用 MongoDB Atlas 在云端设置数据库,以及探索了创建、更新和删除文档的基础。此外,本章详细介绍了聚合管道框架——一个强大的分析工具。

下一章将展示如何使用 FastAPI 创建 API——这是一个令人兴奋且全新的 Python 框架。我们将提供一个最小化但完整的指南,介绍主要的概念和功能,希望这能让你相信构建 API 可以快速、高效且有趣。

第三章:Python 类型提示和 Pydantic

在探索 FastAPI 之前,了解一些将在 FastAPI 旅程中大量使用的 Python 概念是有用的。

Python 类型提示是语言中非常重要且相对较新的特性,它有助于开发者提高工作效率,为开发流程带来更大的健壮性和可维护性。类型使你的代码更易于阅读和理解,最重要的是,它们促进了良好的编程实践。

FastAPI 高度基于 Python 类型提示。因此,在深入研究框架之前,回顾类型提示的基本概念、它们是什么、如何实现以及它们的目的是有用的。这些基础知识将帮助你使用 FastAPI 创建健壮、可维护和可扩展的 API。

到本章结束时,你将对 Python 中类型注解在 FastAPI 和 Pydantic 中的作用有深入的理解。Pydantic 是一个现代 Python 库,它在运行时强制执行类型提示,当数据无效时提供可定制且用户友好的错误,并允许使用 Python 类型注解定义数据结构。

你将能够精确地建模你的数据,利用 Pydantic 的高级功能,使你成为一个更好的、更高效的 FastAPI 开发者。

本章将涵盖以下主题:

  • Python 类型提示及其用法

  • Pydantic 的概述及其主要功能,包括解析和验证数据

  • 数据反序列化和序列化,包括高级和特殊情况

  • 验证和数据转换、别名以及字段和模型级别的验证

  • 高级 Pydantic 使用,例如嵌套模型、字段和模型设置

技术要求

要运行本章中的示例应用程序,你应在本地计算机上安装 Python 版本 3.11.7(https://www.python.org/downloads/)或更高版本,一个虚拟环境,以及一些包。由于本章的示例不会使用 FastAPI,如果你愿意,你可以创建一个干净的虚拟环境,并使用以下命令安装 Pydantic:

pip install pydantic==2.7.1 pydantic_settings==2.2.1

在本章中,你将使用 Pydantic 以及一些与 Pydantic 相关的包,例如pydantic_settings

Python 类型

编程语言中存在的不同类型定义了语言本身——它们定义了其边界,并为可能实现的方式设定了一些基本规则,更重要的是,它们推荐了实现某种功能的方法。不同类型的变量有完全不同的方法和属性集合。例如,将字符串大写是有意义的,但将浮点数或整数列表大写则没有意义。

如果你已经使用 Python 一段时间了,即使是完成最平凡的任务,你也已经知道,就像每一种编程语言一样,它支持不同类型的数据——字符串和不同的数值类型,如整数和浮点数。它还拥有一个相当丰富的数据结构库:从字典到列表,从集合到元组,等等。

Python 是一种动态类型语言。这意味着变量的类型不是在编译时确定的,而是在运行时确定的。这个特性使得语言本身具有很大的灵活性,并允许你将一个变量声明为字符串,使用它,然后稍后将其重新赋值为列表。然而,改变变量类型的便捷性可能会使得更大、更复杂的代码库更容易出错。动态类型意味着变量的类型与其本身嵌入,并且易于修改。

在另一端的是所谓的静态类型语言:C、C++、Java、Rust、Go 等等。在这些语言中,变量的类型是在编译时已知的,并且不能随时间改变。类型检查是在编译时(即在运行时之前)进行的,错误是在运行时之前捕获的,因为编译器会阻止程序编译。

编程语言根据另一个不同的轴划分为不同的类别:强类型语言和弱类型语言。这个特性告诉我们语言对其类型限制到多大程度,以及从一个类型强制转换为另一个类型有多容易。例如,与 JavaScript 不同,Python 被认为是在这个光谱的较强一侧,当你在 Python 解释器中尝试执行非法操作时,解释器会发送强烈的消息,例如在 Python 解释器中输入以下内容以将dict类型添加到数字中:

>>>{}+3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'int'

因此,虽然 Python 会在你尝试执行不支持的操作时抱怨,但它只会在运行时这样做,而不是在执行代码之前。实际上,对于开发者来说,没有任何提示表明你正在编写的代码违反了 Python 的类型系统。

类型提示

正如你在上一节中看到的,Python 是一种动态类型语言,类型是在运行时才知道的。由于变量类型嵌入在变量的值中,作为一个开发者,仅通过查看它或使用你选择的 IDE 检查它,你无法知道代码库中遇到的变量的类型。幸运的是,Python 从 3.5 版本开始引入了一个非常受欢迎的特性——类型注解(https://peps.python.org/pep-0484/)。

类型注解或提示在 Python 中是额外的语法,它通知你,开发者,变量的预期类型。它们在运行时不被 Python 语言使用,并且以任何方式修改或影响你的程序的行为。你可能想知道,如果 Python 解释器甚至看不到它们,这些提示有什么用。

结果表明,几个重要的好处将使几乎任何代码库都更加健壮、更易于维护和面向未来:

  • 更快的代码开发:任何阅读你代码的开发者都会确切知道任何注释变量的类型——无论是整数还是浮点数,列表还是集合,这有助于加快开发速度。

  • 方法和属性知识:你将确切知道任何给定变量可用的哪些方法和属性。在大型代码库中意外更改变量的类型将被立即检测到。

  • 简化代码开发:代码编辑器和 IDE(如 Visual Studio Code)将提供出色的支持和自动完成(IntelliSense),进一步简化开发并减少开发者的认知负荷。

  • 自动代码生成:FastAPI 提供基于 Python 类型提示的自动和交互式(如完全功能的 REST API)文档,完全基于 Python 类型提示。

  • 类型检查器:这是最重要的好处。这些是在后台运行的程序,对你的代码进行静态分析,发现潜在问题并立即通知你。

  • 更易于阅读和更小的认知负荷:注释的代码更容易阅读,并且当你作为开发者需要处理代码并试图弄清楚它应该做什么时,它对你的认知负荷要小得多。

  • 强类型且灵活:保留了语言强类型和动态类型灵活性的特点,同时允许施加必要的安全要求和约束。虽然推荐用于大型代码库,但 Python 类型提示已深入 FastAPI 和 Pydantic,因此即使是小型项目,也至少需要了解类型以及如何使用它们。

类型提示是 FastAPI 的基础。结合 MongoDB 的灵活文档模式,它是 FARM 栈开发的支柱。类型提示确保你的应用程序数据流在系统中的每个时刻都保持正确的数据类型。虽然对于简单的端点来说这可能看起来微不足道——数量应该是整数,名称应该是字符串等——但是当你的数据结构变得更加复杂时,调试类型错误可能会变得非常繁琐。

类型提示也可以被定义为一种形式化——一种在运行时之前(静态)向类型检查器(在你的情况下是 Mypy)指示值类型的正式解决方案,这将确保当 Python 运行时遇到你的程序时,类型不会成为问题。

下一个部分将详细说明类型提示的语法、如何注释函数以及如何使用 Mypy 检查代码。

实现类型提示

让我们看看如何实现类型提示。创建一个名为 Chapter3 的目录,并在其中创建一个虚拟环境,如前所述。在此目录内,如果你想要能够精确地重现章节中的示例,请添加一个包含以下内容的 requirements.txt 文件:

mypy==1.10.0
pydantic==2.7.4

使用 requirements.txt 安装包:

pip install -r requirements.txt

现在,你已经准备好探索 Python 类型提示的世界了。

虽然有许多 Python 类型检查器——基本上是执行源代码静态分析而不运行它的工具——但我们将使用 mypy,因为它易于安装。稍后,你将拥有 Black 或 Ruff 等工具,这些工具会对你的源代码执行不同的操作,包括类型检查。

为了展示 Python 类型注解语法,一个简单的函数,如下所示,就足够了:

  1. 创建一个名为 chapter3_01.py 的文件并定义一个简单的函数:

    def print_name_x_times(name: str, times: int) -> None:
        for _ in range(times):
            print(name)
    

    之前的函数接受两个参数,name(一个字符串)和 times(一个整数),并返回 None,同时该函数会在控制台打印给定名称指定次数。如果你尝试在代码中调用该函数并开始输入参数,Visual Studio Code(或任何具有 Python 类型检查支持的 IDE)会立即建议第一个位置参数为字符串,第二个位置参数为整数。

  2. 你可以尝试输入错误的参数类型,例如,先输入一个整数,然后输入一个字符串,保存文件,并在命令行上运行 mypy

    mypy chapter3_01.py
    
  3. Mypy 将会通知你存在两个错误:

    types_testing.py:8: error: Argument 1 to "print_name_x_times" has incompatible type "int"; expected "str"  [arg-type]
    types_testing.py:8: error: Argument 2 to "print_name_x_times" has incompatible type "str"; expected "int"  [arg-type]
    Found 2 errors in 1 file (checked 1 source file)
    

这个例子足够简单,但再次看看 Python 增强提案 8PEP 8)在另一个例子中对类型提示语法的建议:

  1. 插入一个具有值的简单变量:

    text: str = "John"
    

    冒号紧接在变量后面(没有空格),冒号后有一个空格,并且在提供值的情况下,等号周围有空格。

  2. 当注释函数的输出时,由破折号和大于号组成的“箭头”(->)应该被一个空格包围,如下所示:

    def count_users(users: list[str]) -> int:
        return len(users)
    

    到目前为止,你已经看到了简单的注解,这些注解将变量限制为一些 Python 原始类型,包括整数和字符串。类型注解可以更加灵活:你可能希望允许变量接受几种不同的变量类型,例如整数和字符串。

  3. 你可以使用 typing 模块的 Union 包来实现这一点:

    from typing import Union
    x: Union(str, int)
    
  4. 之前定义的 x 变量可以接受字符串或整数值。实现相同功能的一种更现代和简洁的方式如下:

    x: str | int
    

这些注解意味着变量 x 可以是整数,也可以接受 string 类型的值,这与整数的类型不同。

typing 模块包含几种所谓的泛型,包括以下几种:

  • List:用于应该为列表类型的变量

  • Dict:用于字典

  • Sequence:用于任何类型的值序列

  • Callable:用于可调用对象,例如函数

  • Iterator:表示一个函数或变量接受一个迭代器对象(一个实现迭代器协议并可用于 for 循环的对象)

注意

鼓励你探索 typing 模块,但请记住,该模块中的类型正在逐渐被导入到 Python 的代码功能中。

例如,List 类型在处理 FastAPI 时非常有用,因为它允许你快速高效地将项目或资源的列表序列化为 JSON 输出。

List 类型的例子如下,在一个名为 chapter3_02.py 的新文件中:

from typing import List
def square_numbers(numbers: List[int]) -> List[int]:
    return [number ** 2 for number in numbers]
# Example usage
input_numbers = [1, 2, 3, 4, 5]
squared_numbers = square_numbers(input_numbers)
print(squared_numbers)  # Output: [1, 4, 9, 16, 25]

另一个有用的类型是 Literal,它将变量的可能值限制为几个可接受的状态:

from typing import Literal
account_type: Literal["personal", "business"]
account_type = "name"

前面的几行展示了类型提示的力量。将 account_type 变量分配给字符串本身并没有什么错误,但这个字符串不是可接受状态集的一部分,因此 Mypy 会抱怨并返回一个 Incompatible types in assignment 错误。

现在,看看一个包含 datetime 参数的例子。创建一个名为 chapter3_03.py 的新文件:

from datetime import datetime
def format_datetime(dt: datetime) -> str:
     return dt.strftime("%Y-%m-%d %H:%M:%S")
now = datetime.now()
print(format_datetime(now))

之前定义的函数接受一个参数——一个 datetime 对象,并输出一个字符串:一个格式良好的日期和时间,适用于在网站上显示。如果你在 Visual Studio Code 编辑器中尝试输入 dt 然后一个点,你将收到自动完成系统的提示,提供与 datetime 对象相关的所有方法和属性。

要声明一个结构为字典列表(对任何使用基于 JSON 的 API 的人来说都非常熟悉),你可以使用如下方式,在一个名为 chapter3_04.py 的文件中:

def get_users(id: int) -> list[dict]:
    return [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
        {"id": 3, "name": "Charlie"},
    ]

在介绍了 Python 中的基本注解类型之后,接下来的几节将探讨一些更高级的类型,这些类型在处理 FastAPI 和 Pydantic 时非常有用。

高级注解

你迄今为止看到的注解非常简单,仅传达与变量、函数、类参数或输出相关的特定所需类型的基本信息。Python 的类型系统功能更强大,它可以用来进一步限制允许的变量状态,并防止你,作为开发者,在代码中创建不可能或非法的状态。

最常用的类型如下:

  • Optional 类型用于以明确和开发者友好的方式处理可选值和 None 值。

  • Union 类型允许你定义可能类型的联合,例如整数和字符串。现代 Python 使用管道运算符(|),如前例所示。

  • self 类型用于表示值将是某个类的实例,这在 Pydantic 模型验证器中非常有用,正如我们稍后将要看到的。

  • New 类型允许开发者基于现有类型定义全新的类型。

本节详细介绍了 Python 类型提示、它们的目的以及它们的实现方式。下一节将更深入地探讨 Pydantic,FastAPI 数据验证的得力助手。

Pydantic

Pydantic 是一个数据验证库,在其网站上被标记为 Python 最广泛使用的验证库。它允许您以细粒度的方式对数据进行建模,并在 Python 类型提示系统中牢固地扎根,同时执行各种类型的验证。实际版本 V2 将代码的关键部分重写为Rust以提高速度,并提供了出色的开发者体验。以下列表描述了使用 Pydantic 的一些好处:

  • 基于标准库中的类型提示:您无需学习虚构的新系统或术语,只需学习纯 Python 类型即可。

  • 卓越的速度:FastAPI 和 MongoDB 的各个方面都围绕着速度——以创纪录的时间交付快速且响应迅速的应用程序——因此拥有一个快速的验证和解析库是强制性的。Pydantic 的核心是用 Rust 编写的,这确保了数据操作的高速运行。

  • 庞大的社区支持和广泛采用:当与 Django Ninja、SQLModel、LangChain 等流行包一起工作时,学习 Pydantic 将非常有用。

  • JSON schema 的发射可能性:它有助于与其他系统集成。

  • 更多灵活性:Pydantic 支持不同的模式(在强制转换方面严格和宽松)以及几乎无限定制的选项和灵活性。

  • 深受开发者喜爱:它已被下载超过 7000 万次,PyPI 上有超过 8000 个包依赖于 Pydantic(截至 2024 年 7 月)。

注意

您可以在其文档中详细了解 Pydantic:docs.pydantic.dev/latest/

从广义上讲,Pydantic 在现代 Web 开发工作流程中解决了许多重要问题。它确保输入到您的应用程序中的数据是正确形成和格式化的,位于期望的范围内,具有适当类型和尺寸,并且安全且无错误地到达文档存储库。

Pydantic 还确保您的应用程序输出的数据与预期和规范完全一致,省略了不应公开的字段(如用户密码),甚至包括与不兼容系统交互等更复杂的任务。

FastAPI 站在两个强大的 Python 库——Starlette 和 Pydantic 的肩膀上。虽然 Starlette 负责框架的 Web 相关方面,通常通过 FastAPI 提供的薄包装、实用函数和类来实现,但 Pydantic 负责 FastAPI 的非凡开发者体验。Pydantic 是 FastAPI 的基础,利用其强大的功能为所有 FARM 堆栈开发者打开了竞技场。

虽然类型检查是在静态(不运行代码)的情况下执行的,但 Pydantic 在运行时的作用很明显,并扮演着输入数据的守护者角色。你的 FastAPI 应用将从用户那里接收数据,从灵活的 MongoDB 数据库模式中接收数据,以及通过 API 从其他系统接收数据——Pydantic 将简化解析和数据验证。你不需要为每个可能的无效情况编写复杂的验证逻辑,只需创建与你的应用程序需求尽可能匹配的 Pydantic 模型即可。

在接下来的部分中,你将通过具有递增复杂性和要求的示例来探索 Pydantic 的大部分功能,因为我们认为这是熟悉库的最佳和最有效的方式。

Pydantic 基础知识

与一些提供类似功能的其他库(如dataclasses)不同,Pydantic 提供了一个基类(恰当地命名为BaseModel),通过继承实现了解析和验证功能。由于你将在接下来的部分中构建用户模型,你可以先列出需要与你的用户关联的最基本数据。至少,你需要以下内容:

  • 用户名

  • 电子邮件地址

  • 一个 ID(目前保持为整数)

  • 出生日期

在 Pydantic 中,一个与该规范相关联的用户模型可能如下所示,在一个名为chapter3_05.py的文件中:

from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
    id: int
    username: str
    email: str
    dob: datetime

User类已经为你处理了很多工作——在类实例化时立即执行验证和解析,因此不需要执行验证检查。

构建类的过程相当直接:每个字段都有一个类型声明,Pydantic 准备好通知你任何可能遇到的错误类型。

如果你尝试创建一个用户,你不应该看到任何错误:

Pu = User(id=1, username="freethrow", email="email@gmail.com", dob=datetime(1975, 5, 13))

然而,如果你创建了一个包含错误数据的用户,并且方便地导入了 Pydantic 的ValidationError

from pydantic import BaseModel, ValidationError
try:
    u = User(
        id="one",
        username="freethrow",
        email="email@gmail.com",
        dob=datetime(1975, 5, 13),
    )
    print(u)
except ValidationError as e:
    print(e)

当你运行程序时,Pydantic 会通知你数据无法验证:

1 validation error for User
id
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='one', input_type=str]

Pydantic 的错误信息,源自ValidationError,是故意设计得信息丰富且精确的。出现错误的字段被称作id,错误类型也会被描述。首先想到的有用之处是,如果有多个错误——例如,你可能提供了一个无效的datetime——Pydantic 不会在第一个错误处停止。它会继续解析整个实例,并输出一个错误列表,这个列表可以很容易地以 JSON 格式输出。这实际上是在处理 API 时的期望行为;你希望能够列出所有错误,例如,向发送了错误数据的后端用户。异常包含了一个遇到的所有错误的列表。

模型保证实例在验证通过后,将包含所需的字段,并且它们的类型是正确的。

你还可以根据类型提示约定提供默认值和可空类型:

class User(BaseModel):
    id: int = 2
    username: str
    email: str
    dob: datetime
    fav_colors: list[str] | None = ["red", "blue"]

之前的模型有一个默认的 id 值(这在实践中可能不是你想要做的)以及一个作为字符串的喜欢的颜色列表,这些也可以是 None

当你创建并打印一个模型(或者更准确地说,当你通过 print 函数调用它的表示时),你会得到一个漂亮的输出:

id=2 username='marko' email='email@gmail.com' dob=datetime.datetime(1975, 5, 13, 0, 0) fav_colors=None

Pydantic 默认以宽松模式运行,这意味着它会尝试将提供的类型强制转换为模型中声明的类型。例如,如果你将用户 ID 作为字符串 "2" 传递给模型,将不会出现任何错误,因为 Pydantic 会自动将 ID 转换为整数。

虽然字段可以通过点符号(user.id)访问并且可以轻松修改,但这不建议这样做,因为验证规则将不会应用。你可以创建一个具有 id 值为 5 的用户实例,访问 user.id,并将其设置为字符串 "five",但这可能不是你想要的。

除了纯粹的数据验证之外,Pydantic 还为你的应用程序提供了其他重要的功能。Pydantic 模型中最广泛使用的操作包括以下内容:

  • 数据反序列化:将数据摄入模型

  • 数据序列化:将验证后的数据从模型输出到 Python 数据结构或 JSON

  • 数据修改:实时清理或修改数据

接下来的几节将更详细地查看这些操作的每一个。

反序列化

反序列化是指向模型提供数据的过程,这是输入阶段,与序列化过程相对,序列化意味着以期望的形式输出模型数据。反序列化与验证紧密相关,因为验证和解析过程是在实例化模型时执行的,尽管这可以被覆盖。

在 Pydantic 中,ValidationError 是 Pydantic 类型,当数据无法成功解析为模型实例时会被抛出。

虽然你已经通过实例化基于 Pydantic 的用户模型执行了一些验证,但待验证的数据通常以字典的形式传递。以下是一个将数据作为字典传递的示例,文件名为 chapter3_06.py

创建你的用户模型的另一个版本,并传递一个包含数据的字典:

class User(BaseModel):
    id: int
    username: str
    email: str
    password: str
user = User.model_validate(
    {
        "id": 1,
        "username": "freethrow",
        "email": "email@gmail.com",
        "password": "somesecret",
    }
)
print(user)

.model_validate() 方法是一个辅助方法,它接受一个 Python 字典并执行类实例化和验证。这个方法在一步中创建你的 user 实例并验证数据类型。

类似地,model_validate_json() 接受一个 JSON 字符串(当与 API 一起工作时很有用)。

也可以使用 model_construct() 方法不进行验证来构建模型实例,但这有非常特定的用户场景,并且在大多数情况下不推荐使用。

你已经学会了如何将数据传递给你的简单 Pydantic 模型。下一个部分将更详细地查看模型字段及其属性。

模型字段

Pydantic 字段基于 Python 类型,设置它们为必需或可空并提供默认值是直观的。例如,要为字段创建默认值,只需在模型中提供它作为值即可,而可空字段遵循你在 Python 类型 部分中看到的相同约定——通过使用 typing 模块的旧联合语法,或使用带有管道操作符的新语法。

以下是一个名为 chapter3_07.py 的文件中另一个用户模型的示例:

  1. 插入一些默认值:

    from pydantic import BaseModel
    from typing import Literal
    class UserModel(BaseModel):
        id: int
        username: str
        email: str
        account: Literal["personal", "business"] | None = None
        nickname: str | None = None
    

    之前定义的 UserModel 类定义了一些标准的字符串类型字段:一个账户可以有两个确切值或等于 None,以及一个昵称可以是字符串或 None

  2. 你可以使用 model_fields 属性如下检查模型:

    print(UserModel.model_fields)
    

    你将获得一个方便的列表,其中包含属于该模型的所有字段及其信息,包括它们的类型和是否为必需项:

    {'id': FieldInfo(annotation=int, required=True), 'username': FieldInfo(annotation=str, required=True), 'email': FieldInfo(annotation=str, required=True), 'account': FieldInfo(annotation=Union[Literal['personal', 'business'], NoneType], required=False, default=None), 'nickname': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}
    

下一个部分将详细介绍 Pydantic 特定的类型,这些类型使得使用库更加容易和快速。

Pydantic 类型

虽然 Pydantic 基于标准 Python 类型,如字符串、整数、字典和集合,这使得它对于初学者来说非常直观和简单,但该库还提供了一系列针对常见情况的定制和解决方案。在本节中,你将了解其中最有用的。

严格的类型,如 StrictBoolStrictIntStrictStr 和其他 Pydantic 特定类型,是只有当验证的值属于这些类型时才会通过验证的类型,没有任何强制转换:例如,StrictInt 必须是 Integer 类型,而不是 "1"1.0

限制类型为现有类型提供额外的约束。例如,condate() 是一个具有大于、大于等于、小于和小于等于约束的日期类型。conlist() 包装列表类型并添加长度验证,或可以强制规则,即包含的项必须是唯一的。

Pydantic 不仅限于验证原始类型,如字符串和整数。许多额外的验证器涵盖了你在建模业务逻辑时可能遇到的大多数使用情况。例如,email 验证器验证电子邮件地址,由于它不是 Pydantic 核心包的一部分,因此需要使用以下命令单独安装:

pip install pydantic[email]

Pydantic 网站(https://docs.pydantic.dev/latest/api/types/)提供了一个全面的附加验证类型列表,这些类型扩展了功能——列表可以有最小和最大长度,唯一性可以是必需的,整数可以是正数或负数,等等,例如 CSS 颜色代码。

Pydantic 字段

虽然简单的 Python 类型注解在许多情况下可能足够,但 Pydantic 的真正力量开始在你开始使用 Field 类为字段定制模型并添加元数据到模型字段时显现出来。

让我们看看如何使用上一节中探讨的 UserModelField 类。创建一个文件,并将其命名为 chapter3_08.py

首先,使用 Field 类重写你之前的 UserModel

from typing import Literal
from pydantic import BaseModel, Field
class UserModelFields(BaseModel):
    id: int = Field()
    username: str = Field()
    email: str = Field()
    account: Literal["personal", "business"] | None = Field(default=None)
    nickname: str | None = Field(default=None)

此模型与之前定义的没有字段的模型等效。第一个语法差异可以在提供默认值的方式中看到——Field 类接受一个显式定义的默认值。

字段还通过使用别名提供了额外的模型灵活性,正如你将在下一节中看到的。

字段别名

字段允许你创建和使用别名,这在处理需要与你的基于 Pydantic 的数据定义兼容的不同系统时非常有用。创建一个名为 chapter3_09.py 的文件。假设你的应用程序使用 UserModelFields 模型来处理用户,但也需要能够从另一个系统接收数据,可能通过基于 JSON 的 API,而这个其他系统发送的数据格式如下:

external_api_data = {
    "user_id": 234,
    "name": "Marko",
    "email": "email@gmail.com",
    "account_type": "personal",
    "nick": "freethrow",
}

这种格式明显不符合你的 UserModelFields 模型,而别名提供了一种优雅地处理这种不兼容性的方法:

class UserModelFields(BaseModel):
    id: int = Field(alias="user_id")
    username: str = Field(alias="name")
    email: str = Field()
    account: Literal["personal", "business"] | None = Field(
        default=None, alias="account_type"
    )
    nickname: str | None = Field(default=None, alias="nick")

此更新后的模型为所有具有不同名称的字段提供了别名,因此可以验证你的外部数据:

user = UserModelFields.model_validate(external_api_data)

在这种情况下,你已经使用了简单的 alias 参数,但还有其他选项用于别名,用于序列化或仅用于验证。

此外,Field 类允许以不同的方式约束数值,这是 FastAPI 中广泛使用的一个特性。创建一个名为 chapter3_10.py 的文件并开始填充它。

假设你需要模拟一个具有以下字段的棋类活动:

from datetime import datetime
from uuid import uuid4
from pydantic import BaseModel, Field
class ChessTournament(BaseModel):
    id: int = Field(strict=True)
    dt: datetime = Field(default_factory=datetime.now)
    name: str = Field(min_length=10, max_length=30)
    num_players: int = Field(ge=4, le=16, multiple_of=2)
    code: str = Field(default_factory=uuid4)

在这个相对简单的课程中,Pydantic 字段引入了一些复杂的验证规则,否则这些规则将非常冗长且难以编写:

  • dt:锦标赛的 datetime 对象使用 default_factory 参数,这是一个在实例化时调用的函数,它提供了默认值。在这种情况下,值等于 datetime.now

  • name:此字段有一些长度约束,例如最小和最大长度。

  • 注册球员的数量受到限制:它必须大于或等于 4,小于或等于 16,并且还必须是偶数——2 的倍数,以便所有球员都能在每一轮比赛中进行比赛。

  • uuid 库。

  • id:此字段是一个整数,但这次你应用了 strict 标志,这意味着你覆盖了 Pydantic 的默认行为,不允许像 "3" 这样的字符串通过验证,即使它们可以被转换为整数。

注意

Pydantic 文档中的一个有用页面专门介绍了字段:https://docs.pydantic.dev/latest/concepts/fields/。Field 类提供了许多验证选项,建议在开始建模过程之前浏览一下。

下一节将详细介绍如何通过反序列化过程从模型中获取数据。

序列化

任何解析和验证库最重要的任务是数据序列化(或数据导出)。这是将模型实例转换为 Python 字典或 JSON 编码字符串的过程。生成 Python 字典的方法是 model_dump(),如下面的用户模型示例所示,在一个名为 chapter3_11.py 的新文件中。

要在 Pydantic 中使用电子邮件验证,请将以下行添加到 requirements.txt 文件中:

email_validator==2.1.1

然后,重新运行用户模型:

pip install -r requirements.txt
class UserModel(BaseModel):
    id: int = Field()
    username: str = Field(min_length=5, max_length=20)
    email: EmailStr = Field()
    password: str = Field(min_length=5, max_length=20, pattern="^[a-zA-Z0-9]+$")

您正在使用的用户模型是一个相当标准的模型,并且,凭借您对 Pydantic 字段的了解,您已经可以理解它。有几个新的验证,但它们是直观的:从 Pydantic 导入的 EmailStr 对象是一个验证电子邮件地址的字符串,而 password 字段包含一个额外的正则表达式,以确保该字段只包含字母数字字符,没有空格。以下是一个例子:

  1. 创建模型的一个实例并将其序列化为 Python 字典:

    u = UserModel(
        id=1,
        username="freethrow",
        email="email@gmail.com",
        password="password123",
    )
    print(u.model_dump())
    

    结果是一个简单的 Python 字典:

    {'id': 1, 'username': 'freethrow', 'email': 'email@gmail.com', 'password': 'password123'}
    
  2. 尝试将模型导出为 JSON 表示形式并出于安全原因省略密码:

    print(u.model_dump_json(exclude=set("password"))
    

    结果是一个省略密码的 JSON 字符串:

    {"id":1,"username":"freethrow","email":"email@gmail.com"}
    

序列化默认使用字段名而不是别名,但这是可以通过将 by_alias 标志设置为 True 来轻松覆盖的另一个设置。

在使用 FastAPI 和 MongoDB 时,一个使用的别名示例是 MongoDB 的 ObjectId 字段,它通常序列化为字符串。另一个有用的方法是 model_json_schema(),它为模型生成 JSON 模式。

模型可以通过 ConfigDict 对象进行额外配置,以及一个名为 model_config 的特殊字段——该名称是保留的且必须的。在以下名为 chapter3_12.py 的文件中,您使用 model_config 字段允许通过名称填充模型并防止向模型传递额外的数据:

from pydantic import BaseModel, Field, ConfigDict, EmailStr
class UserModel(BaseModel):
    id: int = Field()
    username: str = Field(min_length=5, max_length=20, alias="name")
    email: EmailStr = Field()
    password: str = Field(min_length=5, max_length=20, pattern="^[a-zA-Z0-9]+$")
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

model_config 字段允许对模型进行额外配置。例如,extra 关键字指的是传递给反序列化过程的数据字段:默认行为是简单地忽略这些数据。

在此示例中,我们将 extra 设置为 forbid,因此任何传递的额外数据(未在模型中声明)将引发验证错误。“populate_by_name” 是另一个有用的设置,因为它允许我们使用字段名而不是仅使用别名来填充模型,实际上是将两者混合使用。您将看到,当构建需要与不同系统通信的 API 时,此功能非常方便。

自定义序列化器

当涉及到序列化时,Pydantic 几乎可以提供无限的能力,并且还提供了不同的序列化方法,用于 Python 和 JSON 输出,这通过使用@field_serializer装饰器来实现。

注意

Python 装饰器是一种强大而优雅的特性,允许您在不更改实际代码的情况下修改或扩展函数或方法的行为。

装饰器是高阶函数,它接受一个函数作为输入,添加一些功能,并返回一个新的、装饰过的函数。这种方法促进了 Python 程序的可重用性、模块化和关注点的分离。

在以下示例中,您将创建一个非常简单的银行账户模型,并使用不同类型的序列化器。您的需求是将余额精确四舍五入到两位小数,并且在序列化为 JSON 时,将updated字段格式化为 ISO 格式:

  1. 创建一个名为chapter3_13.py的新文件,并添加一个简单的银行账户模型,该模型只包含两个字段:余额和最后账户更新时间:

    from datetime import datetime
    from pydantic import BaseModel, field_serializer
    class Account(BaseModel):
        balance: float
        updated: datetime
        @field_serializer("balance", when_used="always")
        def serialize_balance(self, value: float) -> float:
            return round(value, 2)
        @field_serializer("updated", when_used="json")
        def serialize_updated(self, value: datetime) -> str:
           return value.isoformat()
    

    您已添加了两个自定义序列化器。第一个是余额序列化器(如字符串"balance"所示),它将始终被使用。这个序列化器简单地将余额四舍五入到两位小数。第二个序列化器仅用于 JSON 序列化,并将日期返回为 ISO 格式的日期时间字符串。

  2. 如果您尝试填充模型并检查序列化,您将看到序列化器如何修改了初始默认输出:

    account_data = {
        "balance": 123.45545,
        "updated": datetime.now(),
    }
    account = Account.model_validate(account_data)
    print("Python dictionary:", account.model_dump())
    print("JSON:", account.model_dump_json())
    

    您将得到类似的输出:

    Python dictionary: {'balance': 123.46, 'updated': datetime.datetime(2024, 5, 2, 21, 34, 11, 917378)}
    JSON: {"balance":123.46,"updated":"2024-05-02T21:34:11.917378"}
    

在本章的早期部分,您已经看到了通过仅实例化模型类所提供的 Pydantic 基本验证。下一节将讨论 Pydantic 的各种自定义验证方法,以及如何借助 Pydantic 装饰器来利用这些方法,从而超越序列化并提供强大的自定义验证功能。

自定义数据验证

与自定义字段序列化器类似,自定义字段验证器作为装饰器实现,使用@field_validator装饰器。

字段验证器是类方法,因此它们必须接收整个类作为第一个参数,而不是实例,第二个值是要验证的字段名称(或字段列表,或*符号表示所有字段)。

字段验证器应返回解析后的值或一个ValueError响应(或AssertionError),如果传递给验证器的数据不符合验证规则。与其他 Pydantic 功能一样,从示例开始要容易得多。创建一个名为chapter3_14.py的新文件,并插入以下代码:

from pydantic import BaseModel,  field_validator
class Article(BaseModel):
    id: int
    title: str
    content: str
    published: bool
    @field_validator("title")
    @classmethod
    def check_title(cls, v: str) -> str:
        if "FARM stack" not in v:
            raise ValueError('Title must contain "FARM stack"')
        return v.title()

验证器在类实例化之前运行,并接受类和验证的字段名称作为参数。check_title验证器检查标题是否包含字符串"FARM stack",如果不包含,则抛出ValueError。此外,验证器返回标题大写的字符串,因此我们可以在字段级别执行数据转换。

虽然字段验证器提供了很大的灵活性,但它们并没有考虑字段之间的交互和字段值的组合。这就是模型验证器发挥作用的地方,下一节将详细说明。

模型验证器

在执行与网络相关数据的验证时,另一个有用的功能是模型验证——在模型级别编写验证函数的可能性,允许各种字段之间进行复杂的交互。

模型验证器可以在实例化模型类之前或之后运行。我们再次将关注一个相当简单的例子:

  1. 首先,创建一个新文件,并将其命名为chapter3_15.py

  2. 假设你有一个具有以下结构的用户模型:

    from pydantic import BaseModel, EmailStr, ValidationError, model_validator
    from typing import Any, Self
    class UserModelV(BaseModel):
        id: int
        username: str
        email: EmailStr
        password1: str
        password2: str
    

    该模型与之前的模型一样简单,它包含两个密码字段,这两个字段必须匹配才能注册新用户。此外,你还想施加另一个验证——通过反序列化进入模型的 数据不得包含私有数据(如社会保险号码或卡号)。模型验证器允许你执行此类灵活的验证。

  3. 继续上一个模型,你可以在类定义下编写以下模型验证器:

    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        pw1 = self.password1
        pw2 = self.password2
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return self
    @model_validator(mode='before')
    @classmethod
    def check_private_data(cls, data: Any) -> Any:
        if isinstance(data, dict):
            assert (
                'private_data' not in data
            ), 'Private data should not be included'
        return data
    
  4. 现在,尝试验证以下数据:

    usr_data = {
        "id": 1,
        "username": "freethrow",
        "email": "email@gmail.com",
        "password1": "password123",
        "password2": "password456",
        "private_data": "some private data",
    }
    try:
        user = UserModelV.model_validate(usr_data)
        print(user)
    except ValidationError as e:
        print(e)
    

    你只会被告知一个错误——与before模式相关的错误,指出不应包含私有数据。

  5. 如果你取消注释或删除设置private_data字段的行并重新运行示例,错误将变为以下内容:

    Value error, passwords do not match [type=value_error, input_value={'id': 1, 'username': 'fr...ssword2': 'password456'}, input_type=dict]
    

在上一个例子中涉及了一些新概念;你正在使用 Python 的Self类型,它是为了表示包装类的实例而引入的,因此你实际上期望输出是UserModelV类的实例。

check_private_data函数中,还有一个新概念,它检查传递给类的数据是否是字典的实例,然后继续验证字典中是否包含不希望的private_data字段——这只是 Pydantic 检查传递数据的途径,因为它存储在字典内部。

下一节将详细介绍如何使用 Pydantic 组合嵌套模型以验证越来越复杂的模型。

嵌套模型

如果你来自基本的 MongoDB 背景,那么通过组合在 Pydantic 中对嵌套模型的处理非常简单直观。要了解如何实现嵌套模型,最简单的方法是从需要验证的现有数据结构开始,并通过 Pydantic 进行操作:

  1. 从返回汽车品牌和型号(或模型)的 JSON 文档结构开始。创建一个名为 chapter3_16.py 的新文件,并添加以下代码行:

    car_data = {
        "brand": "Ford",
        "models": [
            {"model": "Mustang", "year": 1964},
            {"model": "Focus", "year": 1975},
            {"model": "Explorer", "year": 1999},
        ],
        "country": "USA",
    }
    

    您可以从数据结构内部开始,识别最小的单元或最深层嵌套的结构——在这个例子中,最小的单元是 1964 年的福特野马车型。

  2. 这可以是第一个 Pydantic 模型:

    class CarModel(BaseModel):
        model: str
        year: int
    
  3. 一旦完成这个初步的抽象,创建品牌模型就变得容易了:

    class CarBrand(BaseModel):
        brand: str
        models: List[CarModel]
        country: str
    

汽车品牌型号有独特的名称和产地,并包含一系列车型。

模型字段可以是其他模型(或列表、集合或其他序列)并且这个特性使得将 Pydantic 数据结构映射到数据,尤其是 MongoDB 文档,变得非常愉快和直观。

虽然 MongoDB 可以支持多达 100 层的嵌套,但在您的数据建模过程中,您可能不会达到这个限制。然而,值得注意的是,Pydantic 将在您深入数据结构时支持您。从 Python 端嵌入数据也变得更加容易管理,因为您可以确信进入您集合的数据是按照预期存储的。

下一节和最后一节将详细介绍 Pydantic 提供的另一个有用工具——在处理环境变量和设置时提供一些帮助,这是每个与网络相关的项目都会遇到的问题。

Pydantic Settings

Pydantic Settings 是一个外部包,需要单独安装。它提供了从环境变量或秘密文件中加载设置或配置类的 Pydantic 功能。

这基本上是 Pydantic 网站上的定义(docs.pydantic.dev/latest/concepts/pydantic_settings/),整个概念围绕着 BaseSettings 类展开。

尝试从此类继承的模型会尝试通过扫描环境来读取任何作为关键字参数传递的字段值。

这种简单的功能允许您从环境变量中定义清晰和直接的配置类。Pydantic 设置也可以自动获取环境修改,并在需要时手动覆盖测试、开发或生产中的设置。

在接下来的练习中,您将创建一个简单的 pydantic_settings 设置,这将允许您读取环境变量,并在必要时轻松覆盖它们:

  1. 使用 pip 安装 Pydantic settings:

    pip install pydantic-settings
    
  2. 在与项目文件同一级别的位置创建一个 .env 文件:

    API_URL=https://api.com/v2
    SECRET_KEY=s3cretstr1n6
    
  3. 现在,您可以设置一个简单的 Settings 配置(chapter3_17.py 文件):

    from pydantic import Field
    from pydantic_settings import BaseSettings
    class Settings(BaseSettings):
        api_url: str = Field(default="")
        secret_key: str = Field(default="")
        class Config:
            env_file = ".env"
    print(Settings().model_dump())
    
  4. 如果您运行此代码,Python 和 .env 文件位于同一路径,您将看到 Pydantic 能够从 .env 文件中读取环境变量:

    {'api_url': 'https://api.com/v2', 'secret_key': 's3cretstr1n6'}
    

    然而,如果您设置了环境变量,它将优先于 .env 文件。

  5. 你可以通过在 Settings() 调用之前添加此行来测试它,并观察程序的输出:

    os.environ["API_URL"] = 'http://localhost:8000'
    

Pydantic 设置使得管理配置,如 Atlas 和 MongoDB 的 URL、密码散列的秘密以及其他配置,变得更加结构化和有序。

摘要

本章详细介绍了 Python 的一些方面,这些方面要么是新的且仍在发展中,要么通常被简单地忽视,例如类型提示,以及它们的使用可能对你的项目产生的影响。

FastAPI 基于 Pydantic 和类型提示。与这些稳固的原则和约定一起工作,将使你的代码更加健壮、可维护和面向未来,即使在与其他框架一起工作时也是如此。你已经拥有坚实的 Python 类型基础,并学习了 Pydantic 提供的基本功能——验证、序列化和反序列化。

你已经学会了如何通过 Pydantic 反序列化、序列化和验证数据,甚至在过程中添加一些转换,创建更复杂的结构。

本章已为你提供了学习更多 FastAPI 的网络特定方面的能力,以及如何在 MongoDB、Python 数据结构和 JSON 之间无缝混合数据。

下一章将探讨 FastAPI 及其 Pythonic 基础。

第四章:快速入门 FastAPI

应用程序编程接口API)是您的 FARM 堆栈的基石,作为系统的“大脑”。它实现了业务逻辑,决定了数据如何进出系统,但更重要的是,它如何与系统内的业务需求相关联。

如同 FastAPI 这样的框架,通过示例更容易展示。在本章中,您将探索一些简单的端点,这些端点构成了一个最小、自包含的 REST API。这些示例将帮助您了解 FastAPI 如何处理请求和响应。

本章重点介绍该框架,以及标准 REST API 实践及其在 FastAPI 中的实现。您将学习如何发送请求并根据您的需求修改它们,以及如何从 HTTP 请求中检索所有数据,包括参数和请求体。您还将了解如何处理响应,以及您如何可以使用 FastAPI 轻松设置 cookies、headers 和其他标准网络相关主题。

本章将涵盖以下主题:

  • FastAPI 框架概述

  • 简单 FastAPI 应用的设置和需求

  • FastAPI 中的 Python 特性,例如类型提示、注解和async/await语法

  • FastAPI 如何处理典型的 REST API 任务

  • 处理表单数据

  • FastAPI 项目的结构和路由

技术要求

对于本章,您需要以下内容:

  • Python 设置

  • 虚拟环境

  • 代码编辑器和插件

  • REST 客户端

以下部分将更详细地介绍这些要求。

Python 设置

如果您还没有安装 Python,请访问 Python 下载网站([www.python.org/downloads/](https://www.python.org/downloads/))以获取您操作系统的安装程序。在本书中,您将使用版本 3.11.7或更高版本。

FastAPI 严重依赖于 Python 提示和注解,Python 3.6 之后的版本以类似现代的方式处理类型提示;因此,虽然理论上任何高于 3.6 的版本都应该可以工作,但本书中的代码使用 Python 版本 3.11.7,出于兼容性的原因。

确保您的 Python 安装已升级到最新的 Python 版本之一——如前所述,至少为版本 3.11.7——并且是可访问的且是默认版本。您可以通过以下方式进行检查:

  • 在您选择的终端中键入python

  • 使用pyenv,一个方便的工具,可以在同一台机器上管理多个 Python 版本。

虚拟环境

如果您之前曾经参与过 Python 项目,那么您可能需要包含一些,如果不是几十个,Python 第三方包。毕竟,Python 的主要优势之一在于其庞大的生态系统,这也是它被选为 FARM 堆栈的主要原因之一。

不深入探讨 Python 如何管理第三方包的安装细节,让我们先概述一下,如果你决定为所有项目仅使用一个 Python 安装,或者更糟糕的是,如果这个安装是默认操作系统的 Python 安装,可能会出现的主要问题。

下面是一些挑战:

  • 操作系统在 Python 版本方面通常滞后,所以最新的几个版本可能不可用。

  • 包将安装到相同的命名空间或相同的包文件夹中,这会在任何依赖于该包的应用程序或包中造成混乱。

  • Python 包依赖于其他包,这些包也有版本。假设你正在使用包 A,它依赖于包 B 和 C,并且由于某种原因,你需要将包 B 保持在一个特定的版本(即 1.2.3)。你可能需要包 B 用于完全不同的项目,而这个项目可能需要不同的版本。

  • 减少或无法复现:没有单独的 Python 虚拟环境,将很难快速复制所有必需的包所需的功能。

Python 虚拟环境是解决上述问题的解决方案,因为它们允许你在一个纯净的 Python 开发环境中工作,只包含你需要的包和包版本。在我们的例子中,虚拟环境将肯定包括核心包:FastAPI 和 Uvicorn。另一方面,FastAPI 依赖于 Starlette、Pydantic 等,因此控制包版本非常重要。

Python 开发的最佳实践指出,无论项目大小如何,每个项目都应该有自己的虚拟环境。虽然有多种创建虚拟环境的方法,它是一个分离和独立的 Python 环境,但你将使用virtualenv

使用virtualenv创建新虚拟环境的基本语法如下所示。一旦你处于项目文件夹中,将你的文件夹命名为FARMchapter4,打开一个终端,并输入以下命令:

python – m venv venv

此命令将为你的项目创建一个新的虚拟环境,Python 解释器的副本(或者在 macOS 上,一个全新的 Python 解释器),必要的文件夹结构,以及一些激活和停用环境的命令,以及pip安装程序的副本(pip 用于安装包)。

为了激活你的新虚拟环境,你将根据你的操作系统选择以下命令之一。对于 Windows 系统,在 shell 中输入以下内容:

venv/Scripts/activate

在 Linux 或 macOS 系统上,使用以下命令:

source venv/bin/activate

在这两种情况下,你的 shell 现在应该以你为环境所取的名字作为前缀。在创建新虚拟环境的命令中,最后一个参数是环境名称,所以在这个例子中是venv

在使用虚拟环境时,以下是一些需要考虑的事项:

  • 在虚拟环境放置方面,存在不同的观点。目前,如果你像之前那样将它们保存在项目文件夹内就足够了。

  • activate 命令类似,还有一个 deactivate 命令可以退出你的虚拟环境。

  • requirements.txt 文件中保存确切的包版本并固定依赖项不仅有用,而且在部署时通常是必需的。

Python 社区中有许多 virtualenv 的替代方案,以及许多互补的包。Poetry 是一个同时管理虚拟环境和依赖项的工具,virtualenvwrapper 是一组进一步简化环境管理过程的实用工具。pyenv 稍微复杂一些——它管理 Python 版本,并允许你根据不同的 Python 版本拥有不同的虚拟环境。

代码编辑器

虽然有许多优秀的 Python 代码编辑器和 集成开发环境IDE),但一个常见的选择是微软的 Visual Studio CodeVS Code)。2015 年发布,它是跨平台的,提供了许多集成工具,例如用于运行开发服务器的集成终端。它轻量级,提供了数百个插件,几乎可以满足你任何编程任务的需求。由于你将使用 JavaScript、Python、React 和 CSS 进行样式设计,以及运行命令行进程,因此使用 VS Code 是最简单的方法。

也有一个名为 MongoDB for VS Code 的优秀 MongoDB 插件,它允许你连接到 MongoDB 或 Atlas 集群,浏览数据库和集合,快速查看模式索引,以及查看集合中的文档。这在全栈场景中非常有用,当你发现自己正在处理 Python 的后端代码、JavaScript 和 React 或 Next.js 的前端代码、运行外壳,并需要快速查看 MongoDB 数据库的状态时。扩展程序可在以下链接找到:https://marketplace.visualstudio.com/items?itemName=mongodb.mongodb-vscode。你还可以在 Visual Studio Code 的 扩展 选项卡中通过搜索 MongoDB 来安装它。

终端

除了 Python 和 Git 之外,你还需要一个外壳程序。Linux 和 Mac 用户通常已经预装了一个。对于 Windows,你可以使用 Windows PowerShell 或像 Cmder (cmder.app) 这样的控制台模拟器,它提供了额外的功能。

REST 客户端

为了有效地测试您的 REST API,您需要一个 REST 客户端。虽然Postman(www.postman.com/)功能强大且可定制,但还有其他可行的替代方案。Insomnia()和 REST GUI 提供了一个更简单的界面,而HTTPie(),一个命令行 REST API 客户端,允许在不离开 shell 的情况下快速测试。它提供了诸如表达性语法、表单和上传处理以及会话等功能。

HTTPie 可能是安装最简单的 REST 客户端,因为它可以使用pip或其他包管理器,如 Chocolatey、apt(用于 Linux)或 Homebrew。

安装 HTTPie 的最简单方法是激活您的虚拟环境并使用pip,如下面的命令所示:

pip install httpie

安装完成后,您可以使用以下命令测试 HTTPie:

(venv) http GET "http://jsonplaceholder.typicode.com/todos/1"

输出应该以HTTP/1.1 200 OK响应开始。

venv表示虚拟环境已激活。HTTPie 通过简单地添加POST来简化 HTTP 请求,包括有效载荷、表单值等。

安装必要的包

在设置虚拟环境之后,您应该激活它并安装运行第一个简单应用程序所需的 Python 库:FastAPI 和 Uvicorn。

为了使 FastAPI 运行,它需要一个服务器。在这种情况下,服务器是一种用于提供 Web 应用程序(或 REST API)的软件。FastAPI 依赖于异步服务器网关接口ASGI),它使异步非阻塞应用程序成为可能,这是您可以完全利用 FastAPI 功能的地方。您可以在以下文档中了解更多关于 ASGI 的信息:asgi.readthedocs.io/

目前,FastAPI 文档列出了三个兼容 Python ASGI 的服务器:UvicornHypercornDaphne。本书将重点介绍 Uvicorn,这是与 FastAPI 一起使用最广泛和推荐的选择。Uvicorn 提供高性能,如果您遇到困难,网上有大量的文档可供参考。

要安装前两个依赖项,请确保您位于工作目录中,并激活了所需的虚拟环境,然后执行以下命令:

pip install fastapi uvicorn

现在,您拥有了一个包含 shell、一个或两个 REST 客户端、一个优秀的编辑器和优秀的 REST 框架的 Python 编码环境。如果您之前开发过DjangoFlask应用程序,这些都应该很熟悉。

最后,选择一个文件夹或克隆这本书的 GitHub 仓库,并激活一个虚拟环境。通常,在工作目录中创建一个名为venv的文件夹来创建环境,但请随意根据您的喜好来组织您的目录和代码。

在此之后,本章将简要讨论一些结构化您的 FastAPI 代码的选项。现在,请确保您在一个已激活新创建的虚拟环境的文件夹中。

快速了解 FastAPI

第一章Web 开发和 FARM 栈中,提到了为什么 FastAPI 是 FARM 栈中首选的 REST 框架。使 FastAPI 独特的是其编码速度和由此产生的干净代码,这使得你可以快速发现并修复错误。该框架的作者 Sebastian Ramirez 经常谦逊地强调,FastAPI 只是 Starlette 和 Pydantic 的混合,同时大量依赖现代 Python 特性,特别是类型提示。

在深入示例和构建 FastAPI 应用程序之前,快速回顾 FastAPI 所基于的框架是有用的。

Starlette

Starlette 是一个以高性能和众多特性著称的 ASGI 框架,这些特性在 FastAPI 中也有提供。这些包括 WebSocket 支持、启动和关闭时的事件、会话和 Cookie 支持、后台任务、中间件实现和模板。虽然你不会直接在 Starlette 中编码,但了解 FastAPI 内部的工作原理及其起源是很重要的。

如果你对其功能感兴趣,请访问 Starlette 优秀的文档(https://www.starlette.io/)。

异步编程

你可能在学习使用 Node.js 开发应用程序时已经接触过异步编程范式。这涉及到执行慢速操作,例如网络调用和文件读取,使得系统可以在不阻塞的情况下响应其他请求。这是通过使用事件循环,一个异步任务管理器来实现的,它允许系统将请求移动到下一个,即使前一个请求尚未完成并返回响应。

Python 在 3.4 版本中增加了对异步 I/O 编程的支持,并在 3.6 版本中引入了 async/await 关键字。ASGI 在 Python 世界中随后出现,概述了应用程序应该如何构建和调用,并定义了可以发送和接收的事件。FastAPI 依赖于 ASGI 并返回一个 ASGI 兼容的应用程序。

在这本书中,所有端点函数都带有 async 关键字前缀,甚至在它们成为必要之前,因为你会使用异步的 Motor Python MongoDB 驱动程序。

注意

如果你正在开发一个不需要高压力的简单应用程序,你可以使用简单的同步代码和官方的 PyMongo 驱动程序。

带有 async 关键字的函数是协程;它们在事件循环上运行。虽然本章中的简单示例可能不需要 async 就能工作,但当你通过一个异步驱动程序,如 Motor (https://motor.readthedocs.io/en/stable/),连接到你的 MongoDB 服务器时,FastAPI 中异步编程的真正力量将变得明显。

标准的 REST API 操作

本节将讨论 API 开发中的一些常见术语。通常,通信通过 HTTP 协议进行,通过 HTTP 请求和响应。你将探索 FastAPI 如何处理这些方面,并利用 Pydantic 和类型提示等额外库来提高效率。在示例中,你将使用 Uvicorn 作为服务器。

任何 REST API 通信的基础是一个 URL 和路径的系统。你的本地 Web 开发服务器的 URL 将是http://localhost:8000,因为8000是 Uvicorn 使用的默认端口。端点的路径部分(可选)可以是/cars,而http是方案。你将看到 FastAPI 如何处理路径、查询字符串、请求和响应正文,定义端点函数的特定顺序的重要性,以及如何有效地从动态路径段中提取变量。

在每个路径或地址中,URL 和路径的组合,都有一组可以执行的操作—HTTP 动词。例如,一个页面或 URL 可能列出所有待售的汽车,但你不能发出POST请求,因为这不被允许。

在 FastAPI 中,这些动词作为 Python装饰器实现。换句话说,它们被公开为装饰器,并且只有当你,即开发者,实现它们时,它们才会被实现。

FastAPI 鼓励正确和语义化地使用 HTTP 动词进行数据资源操作。例如,在创建新资源时,你应该始终使用POST(或@post装饰器),对于读取数据(单个或项目列表),使用GET,对于更新使用PATCH等等。

HTTP 消息由请求/状态行、头部和可选的正文数据组成。FastAPI 提供了工具,可以轻松创建和修改头部、设置响应代码以及以干净直观的方式操作请求和响应正文。

本节描述了支撑 FastAPI 性能的编程概念和特定的 Python 特性,使代码易于维护。在下一节中,你将了解标准的 REST API 操作,并了解它们是如何通过 FastAPI 实现的。

FastAPI 是如何表达 REST 的?

观察一个最小的 FastAPI 应用程序,例如经典的Hello World示例,你可以开始检查 FastAPI 如何构建端点。在这个上下文中,端点指定以下详细信息:

  • 一个独特的 URL 组合:这将在你的开发服务器中保持一致—localhost:8000

  • 路径:斜杠后面的部分。

  • HTTP 方法。

例如,在名为Chapter4的新文件夹中,使用 Visual Studio Code 创建一个名为chapter4_01.py的新 Python 文件:

from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
    return {"message": "Hello FastAPI"}

使用这段代码,你可以完成几件事情。以下是每个部分的作用分解:

  • chapter4_01.py的第一行中,你从fastapi包中导入了 FastAPI 类。

  • 接下来,你实例化了一个应用程序对象。这只是一个具有所有 API 功能并暴露一个 ASGI 兼容应用程序的 Python 类,这个应用程序必须传递给 Uvicorn。

现在,应用程序已经准备就绪并实例化。但没有端点,它无法做或说很多。它有一个端点,即根端点,你可以在 http://127.0.0.1:8000/ 上查看。FastAPI 提供了用于 HTTP 方法的装饰器,以告诉应用程序如何以及是否响应。然而,你必须实现它们。

之后,你使用了 @get 装饰器,它对应于 GET 方法,并传递了一个 URL——在这种情况下,使用了根路径 /

装饰的函数,命名为 root,负责响应请求。它接受任何参数(在这种情况下,没有参数)。函数返回的值,通常是 Python 字典,将由 ASGI 服务器转换为 JavaScript 对象表示法JSON)响应,并作为 HTTP 响应返回。这看起来可能很显然,但将其分解以了解基础知识是有用的。

上述代码定义了一个具有单个端点的完整功能应用程序。要测试它,你需要一个 Uvicorn 服务器。现在,你必须使用 Uvicorn 在你的命令行中运行实时服务器:

uvicorn chapter4_01:app --reload

当你使用 FastAPI 进行开发时,你将非常频繁地使用此代码片段,所以以下说明将对其进行分解。

注意

Uvicorn 是你的 ASGI 兼容的 Web 服务器。你可以通过传递可执行 Python 文件(不带扩展名)和实例化应用(FastAPI 实例)的组合(由冒号 : 分隔)来直接调用它。--reload 标志指示 Uvicorn 在你保存代码时每次重新加载服务器,类似于 Node.js 中的 Nodemon。除非指定其他方式,否则你可以使用此语法运行本书中包含 FastAPI 应用的所有示例。

这是使用 HTTPie 测试唯一端点时的输出。记住,当你省略方法的关键字时,它默认为 GET 请求:

(venv) http http://localhost:8000/
HTTP/1.1 200 OK
content-length: 27
content-type: application/json date: Fri, 01 Apr 2022 17:35:48 GMT
server: uvicorn
{
  "message": "Hello FastAPI"
}

HTTPie 通知你你的简单端点正在运行。你将获得 200 OK 状态码,content-type 设置正确为 application/json,并且响应是一个包含所需消息的 JSON 对象。

每个 REST API 指南都以类似的 hello world 示例开始,但使用 FastAPI,这尤其有用。只需几行代码,你就可以看到简单端点的结构。这个端点仅覆盖针对根 URL (/) 的 GET 方法。因此,如果你尝试使用 POST 请求测试此应用,你应该会收到 405 Method Not Allowed 错误(或任何非 GET 方法)。

如果你想要创建一个对 POST 请求返回相同消息的端点,你只需更改装饰器。将以下代码添加到文件末尾(chapter4_01.py):

@app.post("/")
async def post_root():
    return {"message": "Post request success!"}

HTTPie 将在终端中相应地响应:

(venv) http POST http://localhost:8000 HTTP/1.1 200 OK
content-length: 35
content-type: application/json date: Sat, 26 Mar 2022 12:49:25 GMT
server: uvicorn
{
    "message": "Post request success!"
}

现在你已经创建了一些端点,请转到 http://localhost:8000/docs,看看 FastAPI 为你生成了什么。

自动文档

在开发 REST API 时,你会发现你需要不断执行 API 调用——GETPOST 请求——分析响应,设置有效载荷和头信息,等等。选择一个可行的 REST 客户端在很大程度上是一个个人喜好问题,这是一件应该仔细考虑的事情。虽然市场上有很多客户端——从功能齐全的 API IDE,如 Postman (www.postman.com/),到稍微轻量级的 Insomnia (insomnia.rest/) 或 Visual Studio Code 的 REST 客户端 (marketplace.visualstudio.com/items?itemName=humao.rest-client)——本书主要使用非常简单的基于命令行的 HTTPie 客户端,它提供了一个简约的命令行界面。

然而,这正是介绍 FastAPI 最受欢迎的另一个特性的正确时机——交互式文档——这是一个有助于在 FastAPI 中开发 REST API 的工具。

随着你开发的每个端点或路由器,FastAPI 会自动生成文档。它是交互式的,允许你在开发过程中测试你的 API。FastAPI 列出你定义的所有端点,并提供有关预期输入和响应的信息。该文档基于 OpenAPI 规范,并大量依赖于 Python 提示和 Pydantic 库。它允许设置要发送到端点的 JSON 或表单数据,显示响应或错误,与 Pydantic 紧密耦合,并且能够处理简单的授权程序,例如将在 第六章*,认证和授权* 中实现的携带令牌流。你无需使用 REST 客户端,只需打开文档,选择要测试的端点,方便地将测试数据输入到标准网页中,然后点击 提交 按钮!

在本节中,你创建了一个最小化但功能齐全的 API,具有单个端点,让你了解了应用程序的语法和结构。在下一节中,你将了解 REST API 请求-响应周期的基本元素以及如何控制过程的每个方面。标准 REST 客户端提供了一种更可移植的体验,并允许你比较不同的 API,即使它们不是基于 Python 的。

构建展示 API

REST API 围绕 HTTP 请求和响应展开,这些是网络的动力,并且在每个使用 HTTP 协议的 Web 框架中实现。为了展示 FastAPI 的功能,你现在将创建简单的端点,专注于实现所需功能的特定代码部分。而不是常规的 CRUD 操作,接下来的部分将专注于检索和设置请求和响应元素的过程。

获取路径和查询参数

第一个端点将用于通过其唯一的 ID 获取一个虚构的汽车。

  1. 创建一个名为 chapter4_02.py 的文件,并插入以下代码:

    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/car/{id}")
    async def root(id):
        return {"car_id": id}
    car/:id, while {id} is a standard Python string-formatted dynamic parameter in the sense that it can be anything—a string or a number since you haven’t used any hinting.
    
  2. 尝试一下,并使用等于 1 的 ID 来测试端点:

    (venv) http "http://localhost:8000/car/1"
    HTTP/1.1 200 OK
    content-length: 14
    content-type: application/json date: Mon, 28 Mar 2022 20:31:58 GMT
    server: uvicorn
    {
        "car_id": "1"
    }
    
  3. 你收到了你的 JSON 响应,但在这里,响应中的 1 是一个字符串(提示:引号)。你可以尝试用等于字符串的 ID 来执行相同的路由:

    (venv) http http://localhost:8000/car/billy HTTP/1.1 200 OK
    {
        "car_id": "billy"
    }
    

    FastAPI 返回你提供的字符串,它是作为动态参数的一部分提供的。然而,Python 的新特性,如类型提示,也派上用场。

  4. 回到你的 FastAPI 路径(或端点),使汽车 ID 成为整数,只需对变量参数的类型进行提示即可。端点将看起来像这样:

    @app.get("/carh/{id}")
    async def hinted_car_id(id: int):
        return {"car_id": id}
    

你已经给它指定了一个新的路径:/carh/{id}car 后面的 h 表示提示)。除了函数名(hinted_car_id)外,唯一的区别在于参数:紧跟在 int 后面的分号表示你可以期望一个整数,但 FastAPI 对此非常认真,你已经在框架中看到了如何很好地使用提示系统。

如果你查看 http://localhost:8000/docs 上的交互式文档,并尝试在 /carh/ 端点的 id 字段中插入一个字符串,你会得到一个错误。

现在,在你的 REST 客户端中尝试运行它,并通过传递一个字符串来测试 /carh/ 路径。首先,FastAPI 为你正确地设置了状态码——即 422 Unprocessable Entity——并在响应体中指出问题所在——值不是一个有效的整数。它还告知你错误发生的确切位置:在 id 路径中。

这是一个简单的例子,但想象一下你正在发送一个复杂的请求,路径复杂,有多个查询字符串,也许还有头部中的附加信息。使用类型提示可以快速解决这些问题。

如果你尝试访问端点而不指定任何 ID,你将得到另一个错误:

(venv) http http://localhost:8000/carh/ HTTP/1.1 404 Not Found
{
    "detail": "Not Found"
}

FastAPI 再次正确地设置了状态码,给你一个 404 Not Found 错误,并在响应体中重复了此消息。你访问的端点不存在;你必须在斜杠后指定一个值。

可能会出现你拥有类似路径的情况:既有动态路径也有静态路径。一个典型的情况是拥有众多用户的应用程序。将 API 定向到由 /users/id 定义的 URL 将会给你一些关于选定 ID 的用户信息,而 /users/me 通常是一个显示你的信息并允许你以某种方式修改它的端点。

在这种情况下,重要的是要记住,与其他 Web 框架一样,顺序很重要。由于路径处理程序声明的顺序,以下代码将不会产生预期的结果,因为应用程序会尝试将 /me 路径与它遇到的第一个端点匹配——需要 ID 的那个端点——由于 /me 部分不是一个有效的 ID,你会得到一个错误。

创建一个名为 chapter4_03.py 的新文件,并将以下代码粘贴进去:

from fastapi import FastAPI
app = FastAPI()
@app.get("/user/{id}")
async def user(id: int):
    return {"User_id": id}
@app.get("/user/me")
async def me_user():
    return {"User_id": "This is me!"}

当你运行应用程序并测试 /user/me 端点时,你将得到一个与之前相同的 422 Unprocessable Entity 错误。一旦你记住顺序很重要——FastAPI 会找到第一个匹配的 URL,检查类型,并抛出错误。如果第一个匹配的是具有固定路径的那个,那么一切都会按预期工作。只需更改两个路由的顺序,一切就会按预期工作。

FastAPI 对路径处理的一个强大功能是它如何限制路径到一组特定的值和一个从 FastAPI 导入的路径函数,这使你能够在路径上执行额外的验证。

假设你想要一个 URL 路径,它接受两个值并允许以下操作:

  • account_type:可以是 freepro

  • months:这必须是一个介于 3 和 12 之间的整数。

FastAPI 通过让你创建一个基于 Enum 的类来解决这个问题,用于账户类型。这个类定义了账户变量所有可能的值。在这种情况下,只有两个——freepro。创建一个新的文件,并将其命名为 chapter4_04.py,然后编辑它:

from enum import Enum
from fastapi import FastAPI, Path
app = FastAPI()
class AccountType(str, Enum):
    FREE = "free"
    PRO = "pro"

最后,在实际的端点中,你可以将这个类与 Path 函数的实用工具结合起来(不要忘记与 FastAPI 一起从 fastapi 导入它)。将以下代码粘贴到文件的末尾:

@app.get("/account/{acc_type}/{months}")
async def account(acc_type: AccountType, months: int = Path(..., ge=3, le=12)):
    return {"message": "Account created", "account_type": acc_type, "months": months}

在前面的代码中,FastAPI 将路径的 acc_type 部分的类型设置为之前定义的类,并确保只能传递 freepro 值。然而,months 变量是由 Path 实用函数处理的。当你尝试访问这个端点时,account_type 将显示只有两个值可用,而实际的枚举值可以通过 .value 语法访问。

FastAPI 允许你使用标准的 Python 类型声明路径参数。如果没有声明类型,FastAPI 将假设你正在使用字符串。

关于这些主题的更多详细信息,你可以访问优秀的文档网站,看看其他可用的选项(https://fastapi.tiangolo.com/tutorial/path-params/)。在这种情况下,Path 函数接收了三个参数。三个点表示该值是必需的,并且没有提供默认值,ge=3 表示该值可以大于或等于 3,而 le=12 表示它可以小于或等于 12。这种语法允许你在路径函数中快速定义验证。

查询参数

现在你已经学会了如何验证、限制和正确排序你的路径参数和端点,是时候看看查询参数了。这些参数是通过 URL 将数据传递给服务器的简单机制,它们以键值对的形式表示,由等号(=)分隔。你可以有多个键值对,由与号(&)分隔。

查询参数通过在 URL 的末尾使用问号/等号记法添加:?min_price=2000&max_price=4000

问号(?)是一个分隔符,它告诉您查询字符串从哪里开始,而与号(&)允许您添加多个(等号=)赋值。

查询参数通常用于应用过滤器、排序、排序或限制查询集、分页长列表的结果以及类似任务。FastAPI 将它们处理得与路径参数非常相似,因为它会自动提取它们并在您的端点函数中使它们可用于处理。

  1. 创建一个简单的端点,接受两个查询参数,用于汽车的最低价和最高价,并将其命名为chapter4_05.py

    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/cars/price")
    async def cars_by_price(min_price: int = 0, max_price: int = 100000):
        return {"Message": f"Listing cars with prices between {min_price} and {max_price}"}
    
  2. 使用 HTTPie 测试此端点:

    (venv) http "http://localhost:8000/cars/price?min_price=2000&max_price=4000"
    HTTP/1.1 200 OK
    content-length: 60
    content-type: application/json date: Mon, 28 Mar 2022 21:20:24 GMT
    server: uvicorn
    {
    "Message": "Listing cars with prices between 2000 and 4000"
    }
    

在这个解决方案中,您无法确保基本条件,即最低价格应低于最高价格。这是由 Pydantic 的对象级验证处理的。

FastAPI 选择您的查询参数,并执行与之前相同的解析和验证检查。它提供了Query函数,就像Path函数一样。您可以使用大于小于等于条件,以及设置默认值。它们也可以设置为默认为None。根据需要,查询参数将被转换为布尔值。您可以编写相当复杂的路径和查询参数的组合,因为 FastAPI 可以区分它们并在函数内部处理它们。

通过这样,您已经看到了 FastAPI 如何使您能够处理通过路径和查询参数传递的数据,以及它使用的工具在幕后尽快进行解析和验证。现在,您将检查 REST API 的主要数据载体:请求体

请求体——数据的大部分

REST API 允许客户端(一个网页浏览器或移动应用程序)与 API 服务器之间进行双向通信。大部分数据都通过请求和响应体传输。请求体包含从客户端发送到您的 API 的数据,而响应体是从 API 服务器发送到客户端(们)的数据。

这些数据可以用各种方式编码,但许多用户更喜欢使用 JSON 编码数据,因为它与我们的数据库解决方案 MongoDB 非常出色——MongoDB 使用 BSON,与 JSON 非常相似。

当在服务器上修改数据时,您应该始终使用:

  • POST请求:用于创建新资源

  • PUTPATCH:用于更新资源

  • DELETE:用于删除资源

由于请求体将包含原始数据——在这种情况下,MongoDB 文档或文档数组——您可以使用 Pydantic 模型。但首先,看看这个机制是如何工作的,没有任何验证或建模。在 HTTP 术语中,GET方法应该是幂等的,这意味着它应该总是为同一组参数返回相同的值。

在以下用于将新车插入未来数据库的假设端点的代码中,你可以将通用的请求体作为数据传递。它可以是字典,无需进入该字典应该如何构建的细节。创建一个名为 chapter4_06.py 的新文件,并将以下代码粘贴进去:

from typing import Dict
from fastapi import FastAPI, Body
app = FastAPI()
@app.post("/cars")
async def new_car(data: Dict = Body(...)):
    print(data)
    return {"message": data}

直观来看,Body 函数与之前介绍的 PathQuery 函数类似。然而,区别在于,当处理请求体时,此函数是强制性的。

三个点表示请求体是必需的(你必须发送一些内容),但这仅是唯一的要求。尝试插入一辆车(2015 年制造的菲亚特 500):

(venv) http POST "http://localhost:8000/cars" brand="FIAT" model="500" year=2015
HTTP/1.1 200 OK
content-length: 56
content-type: application/json date: Mon, 28 Mar 2022 21:27:31 GMT
server: uvicorn
{
  "message": {
  "brand": "FIAT",
  "model": "500",
  "year": "2015"
}

FastAPI 会做繁重的工作。你可以检索传递给请求体的所有数据,并将其提供给函数以进行进一步处理——数据库插入、可选预处理等。

另一方面,你可以向请求体传递任何键值对。当然,这只是一个说明一般机制的例子——在现实中,Pydantic 将成为你的数据守护者,确保你只让正确的数据进入。

虽然一切顺利,但 FastAPI 仍然会发送一个 200 响应状态,尽管 201 Resource Created 错误更为合适和准确。例如,你可以在函数末尾将一些文档插入 MongoDB,并使用 201 CREATED 状态消息。你将看到修改响应体是多么容易,但就目前而言,你将能够看到 Pydantic 在处理请求体时的优势。

要创建新的汽车条目,你只需要 brandmodel 和生产 year 字段。

因此,在 chapter4_07.py 文件中创建一个简单的 Pydantic 模型,其中包含所需的数据类型:

from fastapi import FastAPI, Body
from pydantic import BaseModel
class InsertCar(BaseModel):
    brand: str
    model: str
    year: int
app = FastAPI()
@app.post("/cars")
async def new_car(data: InsertCar):
    print(data)
    return {"message": data}

到现在为止,你知道前两个参数应该是字符串,而年份必须是整数;它们都是必需的。

现在,如果你尝试发送之前相同的数据,但带有额外的字段,你将只会收到这三个字段。此外,这些字段将经过 Pydantic 解析和验证,如果某些内容不符合数据规范,将抛出有意义的错误信息。

Pydantic 模型验证和 Body 函数的组合,在处理请求数据时提供了所有必要的灵活性。这是因为你可以将它们结合起来,并通过相同的请求总线传递不同的信息片段。

如果你想要传递与用户关联的促销代码以及新车数据,你可以尝试定义一个用于用户的 Pydantic 模型,并使用 Body 函数提取促销代码。首先,在新的文件中定义一个最小的用户模型,并将其命名为 chapter4_08.py

class UserModel(BaseModel):
    username: str
    name: str

现在,创建一个更复杂的函数,该函数将处理两个 Pydantic 模型和可选的用户促销代码——将默认值设置为 None

@app.post("/car/user")
async def new_car_model(car: InsertCar, user: UserModel, code: int = Body(None)):
    return {"car": car, "user": user, "code": code}

对于这个请求,它包含一个完整的 JSON 对象,其中有两个嵌套对象和一些代码,你可能选择使用 Insomnia 或类似的图形用户界面客户端,因为这样做比在命令提示符中输入 JSON 或使用管道要容易。虽然这主要是一个个人偏好的问题,但在开发和测试 REST API 时,拥有一个如 Insomnia 或 Postman 之类的图形用户界面工具以及一个命令行客户端(如 cURL 或 HTTPie)是非常有用的。

Body类构造函数的参数与PathQuery构造函数非常相似,并且由于它们通常会更加复杂,因此尝试使用 Pydantic 来驯服它们是有用的。解析、验证和有意义的错误消息——Pydantic 在允许请求数据到达真实数据处理功能之前为我们提供了整个包。POST请求几乎总是以适当的 Pydantic 模型作为参数传入。

在尝试了请求体和 Pydantic 模型的组合之后,你已经看到你可以控制数据的流入,并且可以确信提供给你的 API 端点的数据将是你想要和期望的数据。然而,有时你可能想要直接与裸金属打交道,并处理原始请求对象。FastAPI 也覆盖了这种情况,如下一节所述。

请求对象

FastAPI 建立在 Starlette 网络框架之上。FastAPI 中的原始请求对象是 Starlette 的请求对象,一旦从 FastAPI 直接导入,你就可以在你的函数中访问它。通过直接使用请求对象,你错过了 FastAPI 最重要的功能:Pydantic 的解析和验证以及自文档化!然而,可能存在你需要拥有原始请求的情况。

看看chapter4_09.py文件中的以下示例:

from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/cars")
async def raw_request(request: Request):
    return {"message": request.base_url, "all": dir(request)}

在前面的代码中,你创建了一个最小的 FastAPI 应用程序,导入了Request类,并在端点中使用它。如果你使用REST客户端测试此端点,你将只得到基础 URL 作为消息,而all部分列出了Request对象的全部方法和属性,以便你了解可用的内容。

所有这些方法和属性都可供你在你的应用程序中使用。

有了这些,你已经看到了 FastAPI 如何帮助你与主要的 HTTP 传输机制——请求体、查询字符串和路径——一起工作。接下来,你将探索任何网络框架解决方案同样重要的方面——cookies、headers、表单数据和文件。

Cookies 和 headers,表单数据,和文件

说到网络框架如何摄取数据,处理表单数据、处理文件以及操作 Cookies 和 headers 等主题必须包括在内。本节将提供 FastAPI 如何处理这些任务的简单示例。

Headers

标头参数的处理方式与查询和路径参数类似,正如你稍后将会看到的,还有 cookie。你可以通过使用Header函数来收集它们,可以说。在诸如身份验证和授权等主题中,标头是必不可少的,因为它们经常携带JSON Web Tokens(JWTs),这些用于识别用户及其权限。

尝试使用新文件chapter4_10.py中的Header函数读取用户代理:

from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/headers")
async def read_headers(user_agent: Annotated[str | None, Header()] = None):
    return {"User-Agent": user_agent}

根据你使用的软件来执行端点的测试,你将得到不同的结果。以下是一个使用 HTTPie 的示例:

(venv) http GET "http://localhost:8000/headers"
HTTP/1.1 200 OK
content-length: 29
content-type: application/json date: Sun, 27 Mar 2022 09:26:49 GMT
server: uvicorn
{
"User-Agent": "HTTPie/3.2.2"
}

你可以用这种方式提取所有标头,FastAPI 将提供进一步的帮助——它将名称转换为小写,将键转换为蛇形命名法,等等。

Cookie

Cookie 的工作方式类似,尽管它们可以从Cookies标头中手动提取。该框架提供了一个名为Cookie的实用函数,它以类似于QueryPathHeader的方式完成所有工作。

表单(和文件)

到目前为止,你只处理了 JSON 数据。它是网络上的通用语言,也是你在数据往返中的主要载体。然而,有些情况需要不同的数据编码——表单可能直接由你的 API 处理,数据编码为multipart/form-dataform-urlencoded。随着现代 React Server Actions 的出现,表单数据在前端开发中也变得更加流行。

注意

尽管你可以在路径操作中声明多个表单参数,但你不能声明 JSON 中预期的Body字段。HTTP 请求将使用仅application/x-www-form-urlencoded而不是application/json进行编码。这种限制是 HTTP 协议的一部分,并不特定于 FastAPI。

覆盖这两种表单情况——包括和不包括上传文件的最简单方法是首先安装python-multipart,这是一个 Python 的流式多部分解析器。为此,你必须停止你的服务器并使用pip来安装它:

pip install python-multipart==0.0.9

Form函数与之前检查的实用函数类似,但不同之处在于它寻找表单编码的参数。对于简单字段,数据通常使用媒体类型(application/x-www-form-urlencoded)进行编码,而如果包含文件,编码对应于multipart/form-data

看一个简单的例子,你希望上传一张图片和一些表单字段,比如品牌和型号。

你将使用一张可以在 Pexels 上找到的照片(www.pexels.com/photo/white-),重命名为car.jpeg并保存在当前目录中。

创建一个名为chapter4_11.py的文件,并将以下代码粘贴进去:

from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload(
    file: UploadFile = File(...), brand: str = Form(...), model: str = Form(...)
):
    return {"brand": brand, "model": model, "file_name": file.filename}

上一段代码通过Form函数处理表单参数,并通过使用UploadFile实用类上传文件。

然而,照片并没有保存在磁盘上——它的存在只是被确认,并返回文件名。在 HTTPie 中测试具有文件上传的端点如下所示:

http -f POST localhost:8000/upload  brand='Ferrari' model='Testarossa'  file@car.jpeg

前面的 HTTPie 调用返回以下输出:

HTTP/1.1 200 OK
content-length: 63
content-type: application/json
date: Fri, 22 Mar 2024 11:01:38 GMT
server: uvicorn
{
    "brand": "Ferrari",
    "file_name": "car.jpeg",
    "model": "Testarossa"
}

要将图像保存到磁盘,你必须将缓冲区复制到磁盘上的实际文件中。以下代码实现了这一点(chapter4_12.py):

import shutil
from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload(
    picture: UploadFile = File(...),
    brand: str = Form(...),
    model: str = Form(...)
):
    with open("saved_file.png", "wb") as buffer:
        shutil.copyfileobj(picture.file, buffer)
    return {"brand": brand, "model": model, "file_name": picture.filename}

open块使用指定的文件名在磁盘上打开一个文件,并复制通过表单发送的 FastAPI 文件。你将硬编码文件名,因此任何新的上传将简单地覆盖现有文件,但你可以使用通用唯一识别码UUID)库等随机生成文件名。

文件上传可以通过不同的方式实现——文件上传也可以由 Python 的async文件库aiofiles或作为后台任务处理,这是 FastAPI 的另一个特性,将在第五章中展示,设置 React 工作流程

FastAPI 响应自定义

前几节讨论了 FastAPI 请求的许多示例,说明了你可以如何触及请求的每一个角落——路径、查询字符串、请求体、头部和 Cookies,以及如何处理表单编码的请求。

现在,让我们更仔细地看看 FastAPI 的响应对象。在所有之前的案例中,你返回了一个由 FastAPI 序列化为 JSON 的 Python 字典。该框架允许对响应进行自定义。

在 HTTP 响应中,你可能首先想要更改的是状态码,例如,在事情没有按计划进行时提供一些有意义的错误。当存在 HTTP 错误时,FastAPI 方便地引发经典的 Python 异常。它还使用符合标准的、有意义的响应代码,以最大限度地减少创建自定义有效负载消息的需求。例如,你不希望为所有内容发送200 OK状态码,然后通过有效负载通知用户错误——FastAPI 鼓励良好的实践。

设置状态码

HTTP 状态码表示操作是否成功或存在错误。这些代码还提供了关于操作类型的信息,并且可以根据几个组来划分:信息性、成功、客户端错误、服务器错误等。虽然不需要记住状态码,但你可能知道404500代码的含义。

FastAPI 使设置状态码变得非常简单——只需将所需的status_code变量传递给装饰器即可。在这里,你正在使用208 status代码为一个简单的端点(chapter4_13.py):

from fastapi import FastAPI, status
app = FastAPI()
@app.get("/", status_code=status.HTTP_208_ALREADY_REPORTED)
async def raw_fa_response():
    return {"message": "fastapi response"}

在 HTTPie 中测试根路由产生以下输出:

(venv) http GET "http://localhost:8000"
HTTP/1.1 208 Already Reported content-length: 30
content-type: application/json date: Sun, 27 Mar 2022 20:14:25 GMT
server: uvicorn
{
    "message": "fastapi response"
}

类似地,你可以为deleteupdatecreate操作设置状态码。

FastAPI 默认设置200 状态码,如果没有遇到异常,因此设置各种 API 操作的正确代码取决于你,例如删除时使用204 No Content,创建时使用201。这是一个特别值得鼓励的良好实践。

Pydantic 可用于响应建模。您可以使用response_model参数限制或修改应在响应中出现的字段,并执行与请求体类似的检查。

FastAPI 不启用自定义响应,但修改和设置头和 cookie 与从 HTTP 请求和框架中读取它们一样简单。

虽然这超出了本书的范围,但值得注意的是,JSON 绝不是 FastAPI 可以提供的唯一响应:您可以输出HTMLResponse并使用经典的 Flask-like Jinja 模板,StreamingResponseFileResponseRedirectResponse等等。

HTTP 错误

错误是不可避免的。例如,用户可能以某种方式向查询发送了错误的参数,前端发送了错误的请求体,或者数据库离线(尽管在 MongoDB 中这种情况不太可能)——任何情况都可能发生。尽快检测这些错误(这是 FastAPI 的一个主题)并向前端以及用户发送清晰完整的消息,通过抛出异常至关重要。

FastAPI 依赖于网络标准,并在开发过程的各个方面强制执行良好实践,因此它非常重视使用 HTTP 状态码。这些代码提供了对出现问题的清晰指示,而有效载荷可以用来进一步阐明问题的原因。

FastAPI 使用一个称为HTTPException的 Python 异常来引发 HTTP 错误。这个类允许您设置状态码并设置错误消息。

回到将新汽车插入数据库的例子,您可以设置一个自定义异常,如下所示(chapter4_14.py):

from pydantic import BaseModel
from fastapi import Fastapi, HTTPException, status
app = FastAPI()
class InsertCar(BaseModel):
    brand: str
    model: str
    year: int
@app.post("/carsmodel")
async def new_car_model(car: InsertCar):
    if car.year > 2022:
        raise HTTPException(
            status.HTTP_406_NOT_ACCEPTABLE, detail="The car doesn't exist yet!"
        )
    return {"message": car}

当尝试插入尚未建造的汽车时,响应如下:

(venv) λ http POST http://localhost:8000/carsmodel brand="fiat" mode3
l="500L" year=2023
HTTP/1.1 406 Not Acceptable content-length: 39
content-type: application/json date: Tue, 29 Mar 2022 18:37:42 GMT
server: uvicorn
{
    "detail": "The car doesn't exist yet!"
}

这是一个相当牵强的例子,用于为可能出现的潜在问题创建自定义异常。然而,这很好地说明了可能实现的内容以及 FastAPI 提供的灵活性。

依赖注入

为了简要但自包含地介绍 FastAPI,必须提到依赖注入系统。从广义上讲,依赖注入DI)是在适当的时间向路径操作函数提供必要功能(类、函数、数据库连接、授权状态等)的一种方式。FastAPI 的 DI 系统对于在端点之间共享逻辑、共享数据库连接等非常有用,正如您在连接到您的 MongoDB Atlas 实例时将看到的——执行安全性和身份验证检查等。

依赖项并不特殊;它们只是可以接受与路径操作相同参数的正常函数。实际上,官方文档将它们与未使用装饰器的路径操作进行比较。尽管如此,依赖项的使用方式略有不同。它们被赋予一个单一参数(通常是可调用的),并且不是直接调用;它们只是作为参数传递给 Depends()

一个受官方 FastAPI 文档启发的示例如下;你可以使用分页依赖并在不同的资源中使用它(chapter4_15.py):

from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
async def pagination(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}
@app.get("/cars/")
async def read_items(commons: Annotated[dict, Depends(pagination)]):
    return commons
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(pagination)]):
    return commons

在全栈 FastAPI 项目中,DI(依赖注入)最常见的情况之一是身份验证;你可以使用相同的身份验证逻辑,即检查头部的授权令牌并将其应用于所有需要身份验证的路由或路由器,正如你将在第六章**,身份验证 和授权 *中看到的那样。

使用路由器结构化 FastAPI 应用程序

虽然将所有我们的 request/response 逻辑放在一个大文件中是可能的,但随着你开始构建一个中等规模的项目,你很快就会看到这并不可行,不可维护,也不便于工作。FastAPI,就像 Node.js 世界的 Express.js 或 Flask 的蓝图,在 /cars 路径上提供了 cars,另一个用于处理 /users 路径上的用户创建和管理,等等。FastAPI 提出了一种简单直观的项目结构,足以容纳最常见的情况。

API 路由器

FastAPI 提供了一个名为 APIRouter 的类,用于分组路由,通常与同一类型的资源(用户、购物项目等)相关。这个概念在 Flask 中被称为 Blueprints,并且在每个现代 Web 框架中都存在,它允许代码更加模块化和分散在更小的单元中,每个路由器只管理一种类型的资源。这些 APIRouter 最终包含在主要的 FastAPI 实例中,并提供非常相似的功能。

而不是直接在主应用程序实例(通常称为 app)上应用路径装饰器(@get@post 等),它们被应用于 APIRouter 实例。下面是一个简单的示例,将应用程序拆分为两个 APIRouter:

  1. 首先,创建一个名为 chapter4_16.py 的文件,该文件将托管主要的 FastAPI 实例:

    from fastapi import FastAPI
    from routers.cars import router as cars_router
    from routers.user import router as users_router
    app = FastAPI()
    app.include_router(cars_router, prefix="/cars", tags=["cars"])
    app.include_router(users_router, prefix="/users", tags=["users"])
    
  2. 现在,创建一个名为 /routers 的新文件夹,并在该文件夹中创建一个名为 users.py 的文件,用于创建 APIRouter:

    from fastapi import APIRouter
    router = APIRouter()
    @router.get("/")
    async def get_users():
        return {"message": "All users here"}
    
  3. 在同一 /routers 目录中创建另一个文件,命名为 cars.py

    from fastapi import APIRouter
    router = APIRouter()
    @router.get("/")
    async def get_cars():
        return {"message": "All cars here"}
    

当在 chapter4_17.py 文件中将路由器连接到主应用程序时,你可以向 APIRouter 提供不同的可选参数——标签和一组依赖项,例如身份验证要求。然而,前缀是强制性的,因为应用程序需要知道在哪个 URL 上挂载 APIRouter。

如果你使用以下命令使用 Uvicorn 测试此应用程序:

uvicorn chapter4_17:app

然后,前往自动生成的文档,您会看到两个 APIRouter 被挂载,就像您定义了两个单独的端点一样。然而,它们被分别归类在各自的标签下,以便于导航和测试。

如果您现在导航到文档,您确实应该找到在/cars上定义的一个路由,并且只响应GET请求。直观地,这个程序可以让您在短时间内构建并行或同一级别的路由,但使用 APIRouters 的最大好处之一是它们支持嵌套,这使得管理端点的复杂层次结构变得轻而易举!

路由是应用程序的子系统,并不打算独立使用,尽管您可以在特定路径下自由挂载整个独立的 FastAPI 应用程序,但这超出了本书的范围。

中间件

FastAPI 实现了请求/响应周期的概念,拦截请求,以某种期望的方式对其进行操作,然后在将其发送到浏览器或客户端之前获取响应,如果需要,执行额外的操作,最后返回最终的响应。

中间件基于 ASGI 规范,并在 Starlette 中实现,因此 FastAPI 允许您在所有路由中使用它,并且可以选择将其绑定到应用程序的一部分(通过 APIRouter)或整个应用程序。

与提到的框架类似,FastAPI 的中间件只是一个接收请求和call_next函数的函数。创建一个名为chapter4_17.py的新文件:

from fastapi import FastAPI, Request
from random import randint
app = FastAPI()
@app.middleware("http")
async def add_random_header(request: Request, call_next):
    number = randint(1,10)
    response = await call_next(request)
    response.headers["X-Random-Integer "] = str(number)
    return response
@app.get("/")
async def root():
    return {"message": "Hello World"}

如果您现在启动这个小型应用程序,并测试唯一的路由,即http://127.0.0.1:8000/上的路由,您会注意到返回的头部包含一个介于 1 到 10 之间的整数,并且每次请求这个整数都会不同。

中间件在跨源资源共享CORS)认证中扮演着重要角色,这是您在开发全栈应用程序时必然会遇到的问题,同时也用于重定向、管理代理等。这是一个非常强大的概念,可以极大地简化并提高您的应用程序效率。

概述

本章介绍了 FastAPI 如何通过利用现代 Python 功能和库(如 Pydantic)实现最常用的 REST API 任务的一些简单示例,以及它如何帮助您。

本章还详细介绍了 FastAPI 如何使您能够通过 HTTP 执行请求和响应,以及您如何在任何时候利用它,自定义和访问请求以及响应的元素。最后,它还详细介绍了如何将 API 拆分为路由,以及如何将应用程序组织成基于资源的逻辑单元。

下一章将为您快速介绍 React——FARM 堆栈中首选的用户界面库。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐