FastAPI教程——Service 层

0 阅读7分钟

中间那个东西是什么来着?
——Otto West,《一条叫旺达的鱼》

预览

本章会展开介绍 Service 层——也就是那个中间的东西。漏水的屋顶可能会花掉很多钱。软件中的“泄漏”没那么明显,但可能会消耗大量时间和精力。你该如何组织自己的应用程序,使各层之间不会泄漏?尤其是,中间的 Service 层中应该放什么,又不应该放什么?

定义一个 Service

Service 层是网站的心脏,是它存在的理由。它接收来自多个来源的请求,访问作为网站 DNA 的数据,并返回响应。

常见的 service 模式包括以下几类操作的组合:

创建 / 检索 / 修改,部分或完整修改 / 删除
一个东西 / 多个东西

在 RESTful 路由层,名词是资源。在本书中,我们的资源最初会包括神秘生物,也就是想象中的生物,以及人,也就是神秘生物探索者。

稍后,还可以定义类似下面这些相关资源:

地点
事件,例如探险、目击记录

布局

下面是当前的文件和目录布局:

main.py
web
├── __init__.py
├── creature.py
├── explorer.py
service
├── __init__.py
├── creature.py
├── explorer.py
data
├── __init__.py
├── creature.py
├── explorer.py
model
├── __init__.py
├── creature.py
├── explorer.py
fake
├── __init__.py
├── creature.py
├── explorer.py
└── test

在本章中,你会折腾 service 目录中的文件。

保护

分层的一个好处是,你不必担心所有事情。Service 层只关心进入数据和从数据出来的内容。正如你将在第 11 章中看到的,更高的一层,在本书中是 Web 层,可以处理认证和授权这些麻烦事。创建、修改和删除函数不应该完全开放,甚至 get 函数最终也可能需要一些限制。

函数

让我们从 creature.py 开始。此时,explorer.py 的需求几乎一样,我们可以借用几乎所有内容。写一个同时处理二者的单个 service 文件非常诱人,但几乎不可避免的是,在某个时候我们会需要以不同方式处理它们。

同样在这个阶段,service 文件几乎就是一个透传层。这属于那种一开始多加一点结构,之后会获得回报的情况。就像你在第 8 章中为 web/creature.pyweb/explorer.py 所做的那样,你会为二者都定义 service 模块,并暂时把它们连接到对应的假数据模块,见示例 9-1 和 9-2。

示例 9-1 一个初始的 service/creature.py 文件

from models.creature import Creature
import fake.creature as data

def get_all() -> list[Creature]:
    return data.get_all()

def get_one(name: str) -> Creature | None:
    return data.get(id)

def create(creature: Creature) -> Creature:
    return data.create(creature)

def replace(id, creature: Creature) -> Creature:
    return data.replace(id, creature)

def modify(id, creature: Creature) -> Creature:
    return data.modify(id, creature)

def delete(id, creature: Creature) -> bool:
    return data.delete(id)

示例 9-2 一个初始的 service/explorer.py 文件

from models.explorer import Explorer
import fake.explorer as data

def get_all() -> list[Explorer]:
    return data.get_all()

def get_one(name: str) -> Explorer | None:
    return data.get(name)

def create(explorer: Explorer) -> Explorer:
    return data.create(explorer)

def replace(id, explorer: Explorer) -> Explorer:
    return data.replace(id, explorer)

def modify(id, explorer: Explorer) -> Explorer:
    return data.modify(id, explorer)

def delete(id, explorer: Explorer) -> bool:
    return data.delete(id)

提示

get_one() 函数返回值的语法,也就是 Creature | None,至少需要 Python 3.9。对于更早版本,你需要使用 Optional

from typing import Optional
...
def get_one(name: str) -> Optional[Creature]:
...

测试!

现在代码库已经开始稍微充实起来,是引入自动化测试的好时机。上一章中的 Web 测试全部都是手动测试。所以,让我们创建一些目录:

test

顶层目录,与 webservicedatamodel 并列。

unit

执行单个函数,但不跨越层边界。

web

Web 层单元测试。

service

Service 层单元测试。

data

Data 层单元测试。

full

也被称为端到端测试或契约测试,这些测试会一次性跨越所有层。它们针对 Web 层中的 API 端点。

这些目录带有 test_ 前缀或 _test 后缀,以供 pytest 使用。你会从示例 9-4 开始看到这一点,该示例会运行示例 9-3 中的测试。

在测试之前,需要做出几个 API 设计选择。如果 get_one() 函数没有找到匹配的 CreatureExplorer,应该返回什么?你可以像示例 9-2 那样返回 None。或者,你也可以抛出异常。Python 的内置异常类型中,没有哪一个直接处理缺失值:

TypeError 可能最接近,因为 NoneCreature 是不同类型。
ValueError 更适用于给定类型下的错误值,不过我想你也可以说,把一个缺失的字符串 id 传给 get_one(id) 符合这种情况。
如果你真的想要,也可以定义自己的 MissingError

无论你选择哪种方法,其影响都会一路冒泡到最顶层。

现在,我们先选择 None 方案,而不是异常。毕竟,none 就是这个意思。示例 9-3 是一个测试。

示例 9-3 Service 测试 test/unit/service/test_creature.py

from model.creature import Creature
from service import creature as code

sample = Creature(name="yeti",
        country="CN",
        area="Himalayas",
        description="Hirsute Himalayan",
        aka="Abominable Snowman",
        )

def test_create():
    resp = code.create(sample)
    assert resp == sample

def test_get_exists():
    resp = code.get_one("yeti")
    assert resp == sample

def test_get_missing():
    resp = code.get_one("boxturtle")
    assert data is None

在示例 9-4 中运行测试。

示例 9-4 运行 service 测试

$ pytest -v test/unit/service/test_creature.py
test_creature.py::test_create PASSED                         [ 16%]
test_creature.py::test_get_exists PASSED                     [ 50%]
test_creature.py::test_get_missing PASSED                    [ 66%]

======================== 3 passed in 0.06s =========================

注意

在第 10 章中,当找不到某个生物时,get_one() 将不再返回 None,因此示例 9-4 中的 test_get_missing() 测试会失败。但到时候会修复它。

其他 Service 层内容

现在我们处在栈的中间——也就是那个真正定义网站目的的部分。而到目前为止,我们只是用它把 Web 请求转发给下一章中的 Data 层。

到目前为止,本书一直以迭代方式开发网站,为未来工作构建一个最小基础。随着你对自己拥有什么、能做什么,以及用户可能想要什么了解得越来越多,你可以开始分支探索并进行实验。有些想法可能只对更大型网站有价值,但下面是一些技术性的网站辅助想法:

日志
指标
监控
追踪

本节会逐一讨论这些内容。我们会在“故障排除”中重新访问这些选项,看看它们能否帮助诊断问题。

日志

FastAPI 会记录每一次对端点的 API 调用,包括时间戳、方法和 URL,但不会记录通过请求体或请求头传递的任何数据。

指标、监控、可观测性

如果你运行一个网站,你大概会想知道它表现如何。对于一个 API 网站,你可能想知道哪些端点正在被访问、有多少人在访问,等等。关于这些因素的统计被称为指标,而对这些指标的收集被称为监控或可观测性。

如今流行的指标工具包括用于收集指标的 Prometheus,以及用于展示指标的 Grafana。

追踪

你的网站表现得有多好?常见情况是,整体指标不错,但这里或那里有些令人失望的结果。或者,整个网站可能都是一团糟。无论哪种情况,拥有一个能够测量一次 API 调用端到端耗时的工具都很有用——而且不仅是总耗时,还包括每个中间步骤的耗时。如果某些东西很慢,你就可以找到链条中的薄弱环节。这就是追踪。

一个新的开源项目把 Jaeger 等早期追踪产品吸收进来,并以 OpenTelemetry 的品牌重新组织。它有一个 Python API,并且至少有一种与 FastAPI 的集成。

要用 Python 安装和配置 OpenTelemetry,请按照 OpenTelemetry Python 文档中的说明操作。

其他

这些生产问题会在第 13 章中讨论。除此之外,我们的领域,也就是神秘生物以及与其相关的任何东西,又该如何呢?除了探索者和生物的基本详情,你还可能想处理什么?你可能会提出一些新想法,而这些想法需要对模型和其他层进行修改。下面是一些你可以尝试的想法:

  • 探索者与他们寻找的生物之间的链接
  • 目击数据
  • 探险
  • 照片和视频
  • Sasquatch 马克杯和 T 恤,见图 9-1

image.png

图 9-1 来自赞助商的一句话

这些类别中的每一种通常都需要定义一个或多个新模型,以及新的模块和函数。其中一些会在第 IV 部分中添加,该部分是一个应用程序画廊,会在第 III 部分构建的基础之上继续添加应用。

回顾

在本章中,你复制了一些来自 Web 层的函数,并移动了它们所使用的假数据。目标是启动新的 Service 层。到目前为止,这还是一个模板化的过程,但此后它会演进并分化。下一章会构建最后的 Data 层,从而得到一个真正运行的实时网站。