框架初体验
FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,专为在 Python 中构建 RESTful API 而设计。
FastAPI 特点
- 高性能: 基于Starlette和Pydantic,利用异步(asynchronous)编程,提供出色的性能。
- 自动文档生成: 自动生成交互式API文档,支持Swagger UI和ReDoc,让API的理解和测试更加直观。
- 类型注解支持: 利用Python的类型提示,提供更严格的输入验证和更好的代码提示。
- 异步支持: 支持异步请求处理,使得处理IO密集型任务更加高效。
FastAPI 适用场景
- 构建API后端: 用于构建RESTful API,支持前后端分离的Web应用。
- 微服务架构: 可以作为微服务的后端框架,支持快速开发和部署。
- 数据处理API: 适用于处理数据,接收和返回JSON数据。
- 实时通信: 支持WebSocket,适用于实时通信场景。
依赖安装
FastAPI 依赖 Python 3.8 及更高版本。
检查当前python版本:
# 检查 Python 版本
python --version
# 或者
python3 --version
安装 FastAPI 很简单,这里我们使用 pip 命令来安装。
pip install fastapi
也可以使用以下命令直接安装 FastAPI 及所有可选依赖:
pip install "fastapi[all]"这会安装:
fastapi- FastAPI 框架uvicorn[standard]- ASGI 服务器python-multipart- 表单和文件上传支持jinja2- 模板引擎python-jose- JWT 令牌支持passlib- 密码哈希bcrypt- 密码加密python-dotenv- 环境变量支持
另外我们还需要一个 ASGI 服务器,生产环境可以使用 Uvicorn 或者 Hypercorn:
pip install "uvicorn[standard]"
启动应用
创建一个名为 main.py 的文件,添加以下代码:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
在命令行中运行以下命令以启动应用:
uvicorn main:app --reload
现在,打开浏览器并访问 **http://127.0.0.1:8000**,你应该能够看到 FastAPI 自动生成的交互式文档,并在根路径 ("/") 返回的 JSON 响应。
[!NOTE]
代码解析:
from fastapi import FastAPI: 这行代码从< code>fastapi 模块中导入了FastAPI类。FastAPI类是 FastAPI 框架的核心,用于创建 FastAPI 应用程序实例。app = FastAPI():这行代码创建了一个 FastAPI 应用实例。@app.get("/"): 这是一个装饰器,用于告诉 FastAPI 哪个 URL 应该触发下面的函数,并且指定了 HTTP 方法为 GET。在这个例子中,它指定了根 URL(即网站的主页)。def read_root():: 这是定义了一个名为read_root的函数,它将被调用当用户使用 GET 方法访问根 URL 时。return {"Hello": "World"}: 这行代码是read_root函数的返回值。当用户使用 GET 方法访问根 URL 时,这个 JSON 对象将被发送回用户的浏览器或 API 客户端。
集成环境创建项目
除了通过依赖下载之外,我们还可以用Pycharm集成直接创建项目:
交互式文档
FastAPI 提供了内置的交互式 API 文档,使开发者能够轻松了解和测试 API 的各个端点。
这个文档是自动生成的,基于 OpenAPI 规范,支持 Swagger UI 和 ReDoc 两种交互式界面。
通过 FastAPI 的交互式 API 文档,开发者能够更轻松地理解和使用 API,提高开发效率
在运行 FastAPI 应用时,Uvicorn 同时启动了交互式 API 文档服务。
默认情况下,你可以通过访问 http://127.0.0.1:8000/docs 来打开 Swagger UI 风格的文档,或者通过 http://127.0.0.1:8000/redoc 来打开 ReDoc 风格的文档。
基本路由
根路径路由
创建 FastAPI 实例和根路径路由:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def Hello():
return {"Hello": "World"}
代码说明:
FastAPI():创建 FastAPI 应用实例。@app.get("/"):使用@app.get装饰器创建一个处理根路径的路由。def Hello():路由处理函数,返回一个包含 {"Hello": "World"} 的字典。
路径参数
设置路由参数:
from fastapi import FastAPI
app = FastAPI()
@app.get("/{id}")
def GetId(id: int,query: Union[str, None] = None):
return {"id": id, "query": query}
代码说明:
@app.get("/{id}"):定义了一个路由路径,其中{id}是路径参数,对应于函数参数id。def GetId(id: int, query: str = None):路由处理函数接受一个整数类型的路径参数id和一个可选的字符串类型查询参数query。- 使用函数参数声明查询参数。例如,query: str = None 表示 query 是一个可选的字符串类型查询参数,默认值为 None。
启动应用和测试路由:
访问 http://127.0.0.1:8000 查看根路径的响应:
{
"Hello": "World"
}
访问 http://127.0.0.1:8000/2?query=ABC 查看带路径参数和查询参数的响应:
{
"id": 2,
"query": "ABC"
}
参数的分类
- 路径参数
- 位置:属于URL路径的一部分
/app/{id} - 作用:指定唯一的、特定的资源
- 方法:
GET
- 位置:属于URL路径的一部分
- 查询参数
- 位置:URL后接
?id=1&key=2 - 作用:对资源集合进行过滤、排序、分页等操作
- 方法:
GET
- 位置:URL后接
- 请求体参数
- 位置:HTTP请求的
消息体body中 - 作用:创建、更新资源携带大量数据
- 方法:
POST、PUT
- 位置:HTTP请求的
类型注解Path
在 FastAPI 中,Path 是一个用于定义和验证路径参数(Path Parameters)的函数。它属于 fastapi 模块,专门用于从 URL 路径中提取变量,并对其进行类型检查、数据验证和文档生成。
使用 Path 可以提供更强大的功能:
- 添加元数据:设置标题、描述、示例值等,用于自动生成 API 文档(Swagger UI / ReDoc)。
- 数据验证:设置最小值 (
gt,ge)、最大值 (lt,le)、正则表达式 (regex)、长度限制等。 - 明确声明:当路径参数没有默认值时,必须使用
Path(...)来显式声明该参数是必需的(尽管在现代 FastAPI 版本中,仅靠类型注解通常也能工作,但使用Path是最佳实践,特别是需要验证时)。
基本用法:
-
作为类型提示的替代或者补充:如果你只需要类型检查,不需要额外的验证或者文档描述,可以直接使用类型注解。写作
Path(...)@app.get("/{id}") def GetId(id: int = Path(..., title="物品ID", description="物品ID的描述")): return {"id": id} -
添加数据验证:可以限制数值的范围或者字符串的格式:
@app.get("/{id}") def GetId(id: int = Path( ..., title="物品ID", description="物品ID的描述", ge=1, # 最小值大于等于1 le=100, # 最大值小于等于100 example=5 # 文档中的示例值 ) ): return {"id": id}gt=0: Great Than ( > 0 )ge=0: Great or Equal ( >= 0 )lt=100: Less Than ( < 100 )le=100: Less or Equal ( <= 100 )
-
字符串验证:对于字符串类型的路径参数,可以使用
regex进行格式正则表达式校验@app.get("/{id}") def GetId(id: str = Path( ..., title="物品ID", description="物品ID的描述", min_length=3, max_length=20, regex=r"^[a-zA-Z0-9_-]+$", # 只允许字母、数字、下划线和连字符 ) ): return {"id": id} -
现代写法:推荐使用
typing.Annotated将类型和校验逻辑分开,这样代码更加清晰,也方便复用验证逻辑:from typing import Annotated # 定义验证逻辑 ItemId = Annotated[int, Path(title="物品ID", gt=0, le=1000)] @app.get("/{id}") def GetId(id: ItemId): return {"id": id}
常见参数列表:
| 参数 | 说明 | 示例 |
|---|---|---|
default | 默认值。对于路径参数,通常设为 ... 表示必需。 | Path(...) |
title | 字段的标题,显示在 API 文档中。 | title="用户ID" |
description | 字段的描述,显示在 API 文档中。 | description="用户的唯一标识" |
gt | 数值必须大于此值。 | gt=0 |
ge | 数值必须大于或等于此值。 | ge=1 |
lt | 数值必须小于此值。 | lt=100 |
le | 数值必须小于或等于此值。 | le=100 |
min_length | 字符串的最小长度。 | min_length=3 |
max_length | 字符串的最大长度。 | max_length=50 |
regex | 用于验证字符串的正则表达式。 | regex=r"^text.*" |
example | 在文档中显示的示例值。 | example=123 |
deprecated | 标记该参数是否已弃用。 | deprecated=True |
类型注解Query
在 FastAPI 中,Query 是用于定义和验证查询参数(Query Parameters)的函数。查询参数是 URL 中 ? 后面的键值对,例如 /items/?skip=0&limit=10 中的 skip 和 limit。
与 Path 类似,Query 允许你:
- 设置默认值:决定参数是可选的还是必需的。
- 数据验证:限制数值范围、字符串长度、正则匹配等。
- 生成文档:为 Swagger UI / ReDoc 提供标题、描述、示例等信息。
- 类型转换:自动将字符串类型的查询参数转换为指定的 Python 类型(如
int,float,bool,list等)。
基本用法:
-
可选参数:如果给参数赋予了默认值(例如
None或者具体数字),FastAPI会自动将其识别为可选的查询参数@app.get("/{id}/") def GetId(id: ItemId, skip: int = Query(default=0, description="跳过的物品数量"), limit: int = Query(default=10, description="返回的最大物品数量") ): return {"id": id, "info": { "skip": skip, "limit": limit } }测试:127.0.0.1:8000/1/?skip=10&limit=1000
{ "id": 1, "info": { "skip": 10, "limit": 1000 } } -
必需参数:如果查询参数出必需的,你需要将默认值设置为
...:int = Query(..., description="跳过的物品数量") -
高级验证:
Query支持所有 Pydantic 的验证规则。from fastapi import FastAPI, Query from typing import Annotated app = FastAPI() @app.get("/users/") async def get_users( # 使用 Annotated 写法 (推荐) age: Annotated[ int | None, Query( title="用户年龄", description="过滤特定年龄的用户", gt=0, # 大于 0 lt=120, # 小于 120 example=25 # 文档示例 ) ] = None, # 传统写法 region: str = Query( default="cn", regex=r"^(cn|us|eu)$", # 只允许 cn, us, eu description="地区代码" ) ): return {"age": age, "region": region}
列表类型查询参数
列表类型的查询参数:可以自动处理重复的查询参数,将其转换成列表:
@app.get("/{id}/")
def GetId(id: ItemId,
item_ids: Annotated[List[int], Query(title="物品ID列表")],
):
return {"id": id,
"list": item_ids
}
-
注意:URL 中需要多次使用同一个键(
?item_ids=1&item_ids=2&item_ids=4),或者使用逗号分隔(取决于客户端和配置,默认是多次键)。{ "id": 1, "list": [2, 3, 4] }
现代写法
现代写法:同样将类型信息与验证逻辑分离,使用typing.Annotated
# 定义验证逻辑
ItemId = Annotated[int, Path(title="物品ID", gt=0, le=1000)]
PaginationSkip = Annotated[int, Query(ge=0, description="跳过记录数", example=0)]
PaginationLimit = Annotated[int, Query(ge=1, le=100, description="每页记录数", example=20)]
SearchQuery = Annotated[str | None, Query(min_length=3, description="搜索关键词")]
@app.get("/{id}/")
def GetId(id: ItemId,
skip: PaginationSkip = 0, # 可以直接用类型,也可以覆盖默认值
limit: PaginationLimit = 20,
q: SearchQuery = None
):
return {"id": id,
"skip": skip,
"limit": limit,
"query": q
}
类型注解Field
在 FastAPI 中,Field 是用于定义 Pydantic 模型(数据模型)内部字段 的验证规则和元数据的函数。
它与 Path 和 Query 的核心区别在于作用域:
Path/Query/Body/Header/Cookie:用于定义 API 接口函数参数 的来源和验证。Field:用于定义 Pydantic 模型类内部属性 的验证规则和文档信息。
当你定义一个继承自 BaseModel 的类(用于请求体 Request Body 或响应体 Response Body)时,你需要使用 Field 来告诉 Pydantic/FastAPI 如何验证该字段的数据,以及如何在 API 文档中展示它。
基本用法
Field 通常作为类型注解的默认值使用。
接下来创建了一个 /items/ 路由,使用 @app.post 装饰器表示这是一个处理 POST 请求的路由:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(..., description="物品的名称", min_length=3, max_length=50)
price: float = Field(..., gt=0, description="物品价格,必须大于0")
tags: list[str] = Field(default_factory=list, description="物品标签列表")
@app.post("/items/")
async def create_item(item: Item):
return item
使用 Pydantic 模型 Item 定义了一个请求体,包含多个字段,其中一些有默认值,更多 Pydantic 介绍参考:FastAPI Pydantic 模型。
数据验证
Field 支持所有 Pydantic 的验证器,这与 Path/Query 非常相似:
- 数值:
gt,ge,lt,le,multiple_of - 字符串:
min_length,max_length,regex(或pattern) - 列表/集合:
min_items,max_items(在 Pydantic V2 中已改为min_length,max_length) - 其他:
title,description,example,examples,deprecated
from pydantic import BaseModel, Field, EmailStr
class UserCreate(BaseModel):
email: EmailStr = Field(..., description="用户邮箱")
age: int = Field(
...,
gt=18,
lt=99,
description="用户年龄必须在 18 到 99 之间"
)
password: str = Field(
...,
min_length=8,
regex=r"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$", # 简单示例:必须包含字母和数字
description="密码至少8位,包含字母和数字"
)
[!TIP]
BaseModel:
BaseModel是 Pydantic 库的核心类,用于数据验证和序列化。它让你可以定义数据模型,并自动验证数据的类型和规则。from pydantic import BaseModel class User(BaseModel): name: str age: int email: str # 自动验证和转换 user = User(name="张三", age=25, email="zhangsan@example.com") print(user.name) # 输出: 张三 print(user.age) # 输出: 25EmailStr:
EmailStr是 Pydantic 提供的特殊类型,用于验证电子邮件格式。它会自动检查字符串是否符合电子邮件格式。from pydantic import BaseModel, EmailStr class User(BaseModel): email: EmailStr # 必须是有效的邮箱格式 # ✅ 正确示例 user1 = User(email="test@example.com") # 成功 user2 = User(email="user@gmail.com") # 成功 # ❌ 错误示例 user3 = User(email="not-an-email") # 报错: 无效的邮箱格式 user4 = User(email="test@") # 报错: 缺少域名 user5 = User(email="@example.com") # 报错: 缺少用户名
字段别名
这是 Field 的一个非常强大的功能。它允许 Python 变量名与 JSON 数据中的键名(如 userName 或 user-name)不一致。
from pydantic import BaseModel, Field
class Product(BaseModel):
# Python 代码中使用 product_id
# JSON 请求体中必须使用 "product-id"
product_id: int = Field(..., alias="product-id")
# Python 代码中使用 item_name
# JSON 请求体中可以使用 "itemName" (驼峰命名)
item_name: str = Field(..., alias="itemName")
# 注意:默认情况下,FastAPI 会使用 alias 来解析传入的数据。
# 如果需要同时支持 alias 和 变量名,可以设置 populate_by_name=True (Pydantic V2)
常量与固定值
如果你希望某个字段在模型中始终是一个固定值,可以使用default固定值并配合frozen=True(可选)
class Cat(BaseModel):
pet_type: str = Field("cat", title="宠物类型", frozen=True) # 始终是 "cat"
name: str
class Dog(BaseModel):
pet_type: str = Field("dog", title="宠物类型", frozen=True) # 始终是 "dog"
name: str
现代写法
官方推荐使用 typing.Annotated 将类型定义与验证逻辑分离。这使得代码更清晰,且方便复用验证规则。
传统写法:
class Item(BaseModel): price: float = Field(..., gt=0, description="价格")
现代写法:
from typing import Annotated
from pydantic import BaseModel, Field
# 定义可复用的类型约束
PositiveFloat = Annotated[float, Field(gt=0, description="必须为正数")]
ShortString = Annotated[str, Field(min_length=1, max_length=50)]
class Item(BaseModel):
price: PositiveFloat
name: ShortString
quantity: Annotated[int, Field(ge=0, default=1)] = 1 # 可以结合默认值
Field vs Path/Query/Body 对比表
| 特性 | Field | Path / Query / Body |
|---|---|---|
| 位置 | Pydantic 模型类内部 (class Item(BaseModel)) | 路径操作函数参数 (def func(param: type = ...)) |
| 用途 | 定义数据结构内部的验证规则和元数据 | 定义 HTTP 请求参数的来源(路径、查询、Body等)及验证 |
| 典型场景 | 请求体 (Request Body) 或 响应体 (Response Body) 的字段定义 | 接收 URL 参数、查询字符串、Header 等 |
| 别名 (Alias) | 常用于处理 JSON 键名与 Python 变量名不一致 | 也可用,但主要用于处理特殊字符参数名 |
| 依赖关系 | 被 Body (隐式或显式) 使用 | 直接使用 |
响应数据
返回类型
默认情况下,FastAPI 会自动将路径操作函数返回的 Python 对象(字典、列表、Pydantic 模型等),经由 jsonable_encoder 转换为 JSON 兼容格式,并包装为 JSONResponse 返回。这省去了手动序列化的步骤,让开发者能更专注于业务逻辑。 如果需要返回非 JSON 数据(如 HTML、文件流),FastAPI 提供了丰富的响应类型来返回不同数据
响应类型设置方式
在 FastAPI 中,设置响应类型(Response Type)主要有两种方式:函数返回类型注解和路径操作装饰器参数。
-
注解:直接在
def或async def后面使用->指定返回类型class Item(BaseModel): name: str price: float is_offer: bool | None = None @app.get("/items/{item_id}") async def read_item(item_id: int) -> Item: # FastAPI 会自动将返回的字典转换为 Item 模型,并验证结构 return {"name": "Foo", "price": 50.2, "is_offer": True} -
response_model参数:在路径操作装饰器(如@app.get)中显式传递response_model参数class Item(BaseModel): name: str price: float @app.get("/items/", response_model=List[Item]) async def read_items(): # 返回一个列表,FastAPI 会验证列表中的每个元素 return [ {"name": "Foo", "price": 50.2}, {"name": "Bar", "price": 60.1} ]
返回JSON 格式
路由处理函数返回一个字典,该字典将被 FastAPI 自动转换为 JSON 格式,并作为响应发送给客户端:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
def read_item(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
返回Pydantic 模型
路由处理函数返回一个 Pydantic 模型实例,FastAPI 将自动将其转换为 JSON 格式,并作为响应发送给客户端:
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.post("/items/")
def create_item(item: Item):
return item
自定义响应数据格式
response_model 是路径操作装饰器(如 @app.get或 @app.post)的关键参数,它通过一个 Pydantic 模型来严格定义和约束 API 端点的输出格式。这一机制在提供自动数据验证和序列化的同时,更是保障数据安全性的第一道防线。
请求头和Cookie
使用 Header 和 Cookie 类型注解获取请求头和 Cookie 数据。
from fastapi import Header, Cookie
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
def read_item(user_agent: str = Header(None), session_token: str = Cookie(None)):
return {"User-Agent": user_agent, "Session-Token": session_token}
异常处理
使用 HTTPException 抛出异常,返回自定义的状态码和详细信息。
以下实例在 item_id 为 42 会返回 404 状态码:
from fastapi import HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id == 42:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
重定向和状态码
RedirectResponse
使用 RedirectResponse 实现重定向,将客户端重定向到 /items/ 路由。
from fastapi import Header, Cookie
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/redirect")
def redirect():
return RedirectResponse(url="/items/")
@app.get("/items/")
def read_item(user_agent: str = Header(None), session_token: str = Cookie(None)):
return {"User-Agent": user_agent, "Session-Token": session_token}
以上代码在浏览器访问 http://127.0.0.1:8000/redirect/ 会自动跳转到 http://127.0.0.1:8000/items/ 页面
处理多个状态码
如果一个接口可能返回多种状态码(如 200 成功,404 未找到),且它们的响应结构不同,可以使用 responses 参数。
class Item(BaseModel):
name: str
class ErrorResponse(BaseModel):
detail: str
@app.get(
"/items/{item_id}",
response_model=Item, # 默认成功响应 (200)
responses={
404: {
"description": "物品未找到",
"model": ErrorResponse, # 404 的响应模型
"content": {
"application/json": {
"example": {"detail": "物品 ID 999 不存在"}
}
}
},
403: {
"description": "无权访问",
"content": {
"application/json": {
"example": {"detail": "权限不足"}
}
}
# 如果没有 model,则只生成文档,不进行数据验证/过滤
}
}
)
async def read_item(item_id: int):
if item_id == 999:
# 直接抛出 HTTPException,FastAPI 会使用上面定义的 404 模型生成文档
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="物品 ID 999 不存在")
return {"name": "My Item"}
自定义响应头
使用 JSONResponse 自定义响应头:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
content = {"item_id": item_id}
headers = {"X-Custom-Header": "custom-header-value"}
return JSONResponse(content=content, headers=headers)
以上代码在浏览器访问 http://127.0.0.1:8000/items/42/ 页面显示如下,可以看到我们自定义的响应头
路径操作依赖项
FastAPI 提供了路径操作依赖项的机制,允许你在路由处理函数执行之前或之后运行一些额外的逻辑,依赖项就是一个函数,且可以使用与路径操作函数相同的参数。路径操作依赖项提供了一种灵活的方式来组织代码、验证输入、进行身份验证等。
依赖项
依赖项:是在路由操作函数执行前或后运行的可复用的函数或对象。
它们被用于执行一些通用的逻辑,如验证、身份验证、数据库连接等。在 FastAPI 中,依赖项通常用于两个方面:
- 预处理(Before)依赖项: 在路由操作函数执行前运行,用于预处理输入数据,验证请求等。
- 后处理(After)依赖项: 在路由操作函数执行后运行,用于执行一些后处理逻辑,如日志记录、清理等。
依赖注入
在 FastAPI 中,通过在路由操作函数参数中声明依赖项来实现依赖注入。
使用方法:
-
定义依赖项
# 依赖项函数 def common_parameters(q: str = None, skip: int = 0, limit: int = 100): return {"q": q, "skip": skip, "limit": limit} -
使用依赖项
from fastapi import Depends # 路由操作函数 @app.get("/items/") async def read_items(commons: dict = Depends(common_parameters)): return commons
在这个例子中,read_items 路由操作函数中的参数 commons 使用了 Depends(common_parameters),表示 common_parameters 是一个依赖项。FastAPI 将在执行路由操作函数之前运行 common_parameters 函数,并将其返回的结果传递给 read_items 函数。
请求路径:
# 1. 最基本的请求
curl "http://localhost:8000/items/"
# 返回: {"q": null, "skip": 0, "limit": 100}
# 2. 搜索关键词
curl "http://localhost:8000/items/?q=手机"
# 返回: {"q": "手机", "skip": 0, "limit": 100}
# 3. 分页查询
curl "http://localhost:8000/items/?skip=20&limit=10"
# 返回: {"q": null, "skip": 20, "limit": 10}
# 4. 搜索加分页
curl "http://localhost:8000/items/?q=电脑&skip=30&limit=15"
# 返回: {"q": "电脑", "skip": 30, "limit": 15}
预处理
以上👆实例中,common_parameters 是一个依赖项函数,它接受查询参数 q、skip 和 limit,并返回一个包含这些参数的字典。
在路由操作函数 read_items 中,通过传入 Depends(common_parameters),我们使用了这个依赖项函数,实现了在路由执行前预处理输入数据的功能。
后处理
以下例子中,after_request 是一个后处理函数,用于在路由执行后执行一些逻辑。
在路由操作函数 read_items_after 中,通过传入 Depends(after_request),我们使用了这个后处理依赖项,实现了在路由执行后进行额外操作的功能。
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
# 后处理函数
async def after_request():
# 这里可以执行一些后处理逻辑,比如记录日志
pass
# 后处理依赖项
@app.get("/items/", response_model=dict)
async def read_items_after(request: dict = Depends(after_request)):
return {"message": "Items returned successfully"}
多依赖组合
以下例子中,common_parameters 和 verify_token 是两个不同的依赖项函数,verify_token 依赖于 common_parameters,这种组合依赖项的方式允许我们在路由执行前先验证一些参数,然后在进行身份验证。
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
# 依赖项函数1
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
# 依赖项函数2
def verify_token(token: str = Depends(common_parameters)):
if token is None:
raise HTTPException(status_code=400, detail="Token required")
return token
# 路由操作函数
@app.get("/items/")
async def read_items(token: dict = Depends(verify_token)):
return token
表单数据
在 FastAPI 中,接收表单数据是一种常见的操作,通常用于处理用户通过 HTML 表单提交的数据。
FastAPI 提供了 Form 类型,可以用于声明和验证表单数据。
声明表单数据
接下来我们设计一个接收一个登陆的表单数据,要使用表单,需预先安装 python-multipart:
pip install python-multipart
添加代码:
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
return {"username": username}
接下来我们可以进入 API 文档 http://127.0.0.1:8000/docs 进行测验。
使用 Pydantic 模型来声明表单数据模型。
在模型中,使用 Field 类型声明每个表单字段,并添加必要的验证规则。
from fastapi import Form, Depends
from pydantic import BaseModel, Field
# 定义模型
class Item(BaseModel):
name: str = Field(..., title="Item Name", max_length=100)
description: str = Field(None, title="Item Description", max_length=255)
price: float = Field(..., title="Item Price", gt=0)
# 创建依赖项函数
async def parse_item_form(
name: str = Form(..., max_length=100),
description: Optional[str] = Form(None, max_length=255),
price: float = Form(..., gt=0)
) -> Item:
# 将收集到的表单数据传递给 Pydantic 模型进行验证和实例化
return Item(name=name, description=description, price=price)
# 在路径中使用
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item = Depends(parse_item_form)):
# 此时 item 已经是验证过的 Item 模型实例了
return {"message": "Success", "data": item}
Model中的
max_length=100(Pydantic验证):class Item(BaseModel): name: str = Field(..., title="Item Name", max_length=100) # 这是 Pydantic 的 Field,用于: # - 数据验证(当创建 Item 实例时) # - 生成 JSON Schema # - API 文档Form依赖中的
max_length=100(FastAPI表单验证):async def parse_item_form( name: str = Form(..., max_length=100), # 这是 FastAPI 的 Form # 这是 FastAPI 的表单验证: # - 在请求到达时立即验证表单数据 # - 如果超过100字符,直接返回422错误 # - 不会继续执行函数 ) -> Item: return Item(name=name, ...) # 此时 name 已经确保不超过100字符
接收表单数据
在路由操作函数中,可以使用 Form 类型来接收表单数据。
Form 类型的参数可以与 Pydantic 模型的字段一一对应,以实现表单数据的验证和转换。
from fastapi import FastAPI, Form
app = FastAPI()
# 路由操作函数
@app.post("/items/")
async def create_item(
name: str = Form(...),
description: str = Form(None),
price: float = Form(..., gt=0),
):
return {"name": name, "description": description, "price": price}
以上例子中,create_item 路由操作函数接收了三个表单字段:name、description 和 price,这些字段与 Item 模型的相应字段一致,FastAPI 将自动根据验证规则验证表单数据。
接下来我们可以进入 API 文档 http://127.0.0.1:8000/docs 进行测验。
文件上传
如果表单包含文件上传,可以使用 UploadFile 类型处理。
以下是一个处理文件上传的实例:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
# 路由操作函数
@app.post("/files/")
async def create_file(file: UploadFile = File(...)):
return {"filename": file.filename}
在这个例子中,create_file 路由操作函数接收了一个 UploadFile 类型的文件参数。
FastAPI 将负责处理文件上传,并将文件的相关信息包装在 UploadFile 对象中,可以轻松地获取文件名、内容类型等信息。
核心概念
FastAPI 是一个现代、快速(高性能)的 Python Web 框架,用于构建 API。它基于标准 Python 类型提示,使用 Starlette 和 Pydantic 构建。
异步编程
同步与异步核心概念:
import asyncio import time import httpx # 同步方式 - 阻塞执行 def sync_fetch_data(): start_time = time.time() # 模拟三个网络请求 time.sleep(1) # 第一个请求 time.sleep(1) # 第二个请求 time.sleep(1) # 第三个请求 print(f"同步执行耗时: {time.time() - start_time:.2f}秒") # 约3秒 # 异步方式 - 并发执行 async def async_fetch_data(): start_time = time.time() # 三个请求并发执行 await asyncio.gather( asyncio.sleep(1), # 第一个请求 asyncio.sleep(1), # 第二个请求 asyncio.sleep(1), # 第三个请求 ) print(f"异步执行耗时: {time.time() - start_time:.2f}秒") # 约1秒 # 运行示例 sync_fetch_data() # 输出: 同步执行耗时: 3.00秒 asyncio.run(async_fetch_data()) # 输出: 异步执行耗时: 1.00秒
ASGI是 Python 异步 Web 服务器和应用程序之间的标准接口。
# WSGI 应用(同步)- 传统 Flask 风格
def wsgi_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [b'Hello World']
# ASGI 应用(异步)- FastAPI 风格
async def asgi_app(scope, receive, send):
await send({
'type': 'http.response.start',
'status': 200,
'headers': [(b'content-type', b'text/plain')],
})
await send({
'type': 'http.response.body',
'body': 'Hello World',
})
FastAPI 中的异步路径操作:
from fastapi import FastAPI
import asyncio
app = FastAPI()
# 同步路径操作
@app.get("/sync")
def sync_endpoint():
# 同步操作会阻塞整个应用
time.sleep(2)
return {"message": "同步响应"}
# 异步路径操作(推荐)
@app.get("/async")
async def async_endpoint():
# 异步操作不会阻塞其他请求
await asyncio.sleep(2)
return {"message": "异步响应"}
[!TIP]
何时使用异步?
适合异步的场景:
- 数据库操作
- 网络请求(API 调用)
- 文件 I/O 操作
- 长时间等待的操作
不适合异步的场景:
- CPU 密集型计算
- 简单的数据处理
- 没有 I/O 等待的操作
REST API
REST是一种 Web API 设计风格,强调:
- 资源导向:URL 表示资源
- 状态无关:每个请求都是独立的
- 统一接口:使用标准 HTTP 方法
- 分层系统:支持缓存、负载均衡等
Http方法
from fastapi import FastAPI, status, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
# GET - 安全且幂等
@app.get("/users/{user_id}")
async def get_user(user_id: int):
"""获取资源,不修改服务器状态"""
user = find_user(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
# POST - 不安全且非幂等
@app.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
"""创建新资源"""
if user_exists(user.email):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already exists"
)
new_user = create_new_user(user)
return new_user
# PUT - 不安全但幂等
@app.put("/users/{user_id}")
async def replace_user(user_id: int, user: UserUpdate):
"""完整替换资源"""
if not user_exists(user_id):
# PUT 可以创建资源
return create_user_with_id(user_id, user)
return replace_existing_user(user_id, user)
# PATCH - 不安全且通常非幂等
@app.patch("/users/{user_id}")
async def update_user(user_id: int, user: UserPatch):
"""部分更新资源"""
existing_user = find_user(user_id)
if not existing_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return patch_user_fields(existing_user, user)
# DELETE - 不安全但幂等
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
"""删除资源"""
if not user_exists(user_id):
# 幂等性:删除不存在的资源仍返回成功
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)
delete_existing_user(user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)
Http状态码
常用状态码速查:
# 成功状态码
200 OK # 请求成功
201 Created # 资源创建成功
204 No Content # 成功但无内容返回
206 Partial Content # 部分内容(分页、断点续传)
# 重定向
301 Moved Permanently # 永久重定向
302 Found # 临时重定向
304 Not Modified # 资源未修改(缓存)
# 客户端错误
400 Bad Request # 请求格式错误
401 Unauthorized # 未认证
403 Forbidden # 已认证但无权限
404 Not Found # 资源不存在
405 Method Not Allowed # HTTP 方法不允许
409 Conflict # 资源冲突
422 Unprocessable Entity # 数据验证失败
429 Too Many Requests # 请求过于频繁
# 服务器错误
500 Internal Server Error # 服务器内部错误
502 Bad Gateway # 网关错误
503 Service Unavailable # 服务不可用
JSON数据最佳实践
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from pydantic import BaseModel, Field
import json
# JSON 数据格式规范
class UserResponse(BaseModel):
id: int
username: str
email: str
full_name: Optional[str] = None
is_active: bool = True
created_at: datetime
updated_at: Optional[datetime] = None
# JSON 序列化配置
class Config:
# 允许使用字段别名
allow_population_by_field_name = True
# JSON 编码器
json_encoders = {
datetime: lambda v: v.isoformat(),
Decimal: lambda v: float(v)
}
# 响应格式标准化
class APIResponse(BaseModel):
"""标准 API 响应格式"""
success: bool = True
message: str = "操作成功"
data: Optional[dict] = None
errors: Optional[List[str]] = None
timestamp: datetime = Field(default_factory=datetime.now)
# 分页响应格式
class PaginatedResponse(BaseModel):
items: List[dict]
total: int
page: int
size: int
pages: int
@app.get("/users", response_model=PaginatedResponse)
async def get_users(page: int = 1, size: int = 10):
"""返回分页的用户列表"""
users = get_users_paginated(page, size)
total = count_users()
return PaginatedResponse(
items=users,
total=total,
page=page,
size=size,
pages=(total + size - 1) // size
)
自动文档生成
from fastapi import FastAPI, Query, Path, Body
from fastapi.openapi.utils import get_openapi
app = FastAPI(
title="我的 API",
description="这是一个示例 API,展示 FastAPI 的功能",
version="1.0.0",
terms_of_service="http://example.com/terms/",
contact={
"name": "开发者",
"url": "http://example.com/contact/",
"email": "developer@example.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
)
@app.get(
"/users/{user_id}",
summary="获取用户信息",
description="根据用户 ID 获取用户的详细信息",
response_description="用户信息对象",
tags=["用户管理"]
)
async def get_user(
user_id: int = Path(..., title="用户ID", description="要获取的用户ID", ge=1),
include_posts: bool = Query(False, title="包含文章", description="是否包含用户的文章列表")
):
"""
获取用户信息:
- **user_id**: 用户的唯一标识符
- **include_posts**: 是否在响应中包含用户的文章列表
返回用户的基本信息,如果 include_posts 为 True,还会包含文章列表。
"""
user = find_user(user_id)
if include_posts:
user.posts = get_user_posts(user_id)
return user
# 自定义 OpenAPI schema
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="自定义 API 文档",
version="2.5.0",
description="这是自定义的 OpenAPI schema",
routes=app.routes,
)
# 添加自定义信息
openapi_schema["info"]["x-logo"] = {
"url": "https://example.com/logo.png"
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
项目结构
一个标准的、能长大的 FastAPI 项目结构是这样的(这是 2026 年业界最推荐的模块化结构):
标准项目结构:
crud/:数据库增删该查逻辑(封装数据库操作)models/:数据库模型routers/:路由层schemas/:数据验证模型utils/:工具函数config/:配置相关main.py:主入口文件
模块化路由
创建模块化目录结构:
app/
---routers/
------路由1/
------路由2/
编写独立路由模块:
rom fastapi import APIRouter
# 创建 APIRouter 实例
router = APIRouter(prefix="/api/news",tags=["news"])
@router.get("/categories")
async def get_categories():
return {"msg": "获取分类成功"
prefix:路由前缀tags:交互式文档分组
在主入口文件中注册路由:
from fastapi import FastAPI
from routers import news
app = FastAPI()
# 注册路由
app.include_router(news.router)
中间件
中间件(Middleware)是一个在每次请求进入 FastAPI 应用时都会被执行的函数。 它在请求到达实际的路径操作(路由处理函数)之前运行,并且在响应返回给客户端之前再运行一次。
常用的中间件处理情况:
- 跨域处理
- 身份认证
- 响应头处理
- 性能监控
- 日志处理
基本使用:
函数的顶部使用装饰器 @app.middleware("http")
语法格式:
@app.middleware("http")
async def 中间件函数(request,call_next):
respone = await call_next(request)
return respone
request:请求对象,包含请求的所有信息(headers, body, method, url 等)。call_next:一个可调用对象(函数)。你必须调用它并传入request,它会将请求传递给后续的中间件或实际的路径操作函数,并返回一个Response对象。如果不调用它,请求链条就会中断,用户永远收不到响应。- 异步: 中间件函数必须是
async def。 - 返回值: 必须返回一个
Response对象(通常是call_next返回的那个,或者是基于它修改后的对象)。
依赖注入和中间件的区别
- 中间件:像是一个全局的过滤器,包裹着整个应用。它处理的是原始的 HTTP 请求和响应(Request/Response 对象),通常用于与具体业务逻辑无关的基础设施任务(如 CORS、Gzip、全局日志、IP 黑名单)。
- 依赖注入:像是路径操作函数的参数准备器。它在路由被匹配后执行,处理的是Python 对象(如数据库会话、当前用户对象、分页参数),主要用于业务逻辑的解耦和复用。
| 特性 | 中间件 (Middleware) | 依赖注入 (Depends) |
|---|---|---|
| 作用范围 | 全局。对应用接收到的每一个请求生效(除非你在代码里手动写 if 排除某些路径)。 | 局部。仅对声明了该依赖的特定路径操作函数(或包含该依赖的其他依赖)生效。 |
| 操作对象 | Starlette/FastAPI 的 Request 和 Response 对象。操作的是底层的 HTTP 协议数据(Header, Body, Status Code)。 | 普通的 Python 对象。你可以返回任何类型(dict, class instance, DB session, str 等)。 |
| 执行时机 | 在路由匹配之前就开始执行(请求阶段),在响应返回给客户端之前最后执行(响应阶段)。 | 在路由匹配之后,实际的路径操作函数执行之前。 |
| 主要用途 | 跨切面基础设施: • CORS / Gzip • 全局请求/响应日志 • 简单的 IP 拦截 • 修改全局 Header | 业务逻辑复用: • 数据库连接管理 • 用户身份验证与授权(获取 current_user) • 参数验证与转换 • |
跨域处理
跨域资源共享(CORS)是一种浏览器安全机制,用于允许运行在一个源(Origin)的 Web 应用,通过浏览器向另一 个源的服务器发起跨域 HTTP 请求,并在服务器授权的前提下获取资源。
解决办法:CORS中间件,让后端主动告诉浏览器:这个前端“允许访问”
from fastapi.middleware.cors import CORSMiddleware
# 允许的来源(可以是域名列表)
origins = [
"http://localhost",
"http://localhost:3000",
"https://your-frontend-domain.com"
]
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许访问的源
allow_credentials=True, # 允许携带 Cookie
allow_methods=["*"], # 允许所有请求方法
allow_headers=["*"], # 允许所有请求
)
ORM
ORM(Object-RelationalMapping,对象关系映射)是一种编程技术,用于在面向对象编程语言和关系型数据库之间 建立映射。它允许开发者通过操作对象的方式与数据库进行交互,而无需直接编写复杂的SQL语句。
优势:
- 减少重复的 SQL 代码
- 代码更简洁易读
- 自动处理数据库连接和事务
- 自动防止 SQL 注入攻击
ORM配置
# 安装 SQLAlchemy + asyncio 驱动
pip install sqlalchemy[asyncio] aiomysql
-
(核心库):这是 Python 最著名的 SQL 工具包和 ORM(对象关系映射)框架 -
asyncio:开启异步支持 -
aiomysql:是一个第三方的、纯 Python 编写的 MySQL 驱动程序
创建数据库引擎
# 1. 导入 SQLAlchemy 的异步组件
from sqlalchemy.ext.asyncio import create_async_engine
# 2. 配置数据库连接模型 格式:mysql+aiomysql://用户:密码@host:port/库名
DB_URL = "mysql+aiomysql://root:123456@localhost:3306/testdb"
# 3. 创建引擎
DB_ENGINE = create_async_engine(
DB_URL,
echo=True, # 是否输出SQL日志
pool_size = 10, # 设置连接池中保持的持久连接数
max_overflow = 20 # 设置连接池允许创建的额外连接数
)
定义数据模型
定义基类
基类,继承 DeclarativeBase(包含通用属性和字段的映射)
from sqlalchemy.orm import DeclarativeBase
# 定义模型类:基类
class Base(DeclarativeBase):
pass
创建映射
使用 SQLAlchemy 2.0 的 Mapped 和 mapped_column 语法,这是类型安全的新标准。
语法格式:
列名: Mapped[T] = mapped_column(列属性)
1. Mapped[T]:
- 作用:这是一个类型提示泛型。它告诉 Python 类型检查器和 ORM:“这个属性的数据类型是
T”。 - 位置:放在变量名的左侧,作为类型注解。
- 示例:
id: Mapped[int]表示id属性在 Python 代码中是一个整数。
2. mapped_column():
- 作用:这是一个函数,用于定义数据库列的具体行为(如主键、外键、是否可空、默认值等)。它替代了旧的
Column()。 - 位置:放在变量名的右侧,作为赋值表达式。
- 示例:
mapped_column(Integer, primary_key=True)告诉 SQLAlchemy 在数据库中创建一个整数类型的主键列。 - 参数:
mapped_column的参数非常灵活,主要分为三类:- 类型相关:通常是 SQLAlchemy 的类型类(如
Integer,String)。 - 约束相关:这些参数直接映射到 DDL
primary_key=True: 主键。unique=True: 唯一约束。nullable=False: 非空约束 (NOT NULL)。index=True: 创建索引。ForeignKey(...): 外键约束。
- 默认值相关:
default=value: Python 端默认值。当你执行user = User()且没传该字段时生效。如果直接执行 SQL 插入,此值不生效。
- 类型相关:通常是 SQLAlchemy 的类型类(如
路由中使用ORM
创建工厂函数:
创建异步 Session 工厂
# class_=AsyncSession: 指定使用异步会话
AsyncSessionLocal = async_sessionmaker(
bind=DB_ENGINE, # 绑定数据库引擎
class_=AsyncSession, # 指定会话类
expire_on_commit=False # 会话对象不过期,不重新查询数据库
)
添加依赖项,用于获取依赖会话
依赖注入函数 (FastAPI 的核心)
# 这个函数会被 FastAPI 的 Depends() 调用
# 使用 yield 确保会话在使用完毕后自动关闭,即使发生异常
async def Get_Database() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session # 返回数据库会话给路由处理函数
await session.commit() # 无异常提交事务
except Exception:
await session.rollback() # 有异常则回滚
raise
finally:
await session.close() # 关闭会话
路由中使用:
@app.get("/book/books")
async def get_book_list(
db: AsyncSession = Depends(Get_Database)
):
# 数据库操作
pass
项目中配置
通常我们会在项目的config目录下新建一个db.py作为数据库orm配置文件,可以直接拷贝以下👇代码:
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine
# 数据库URL
ASYNC_DATABASE_URL = "mysql+aiomysql://数据库账号:数据库密码@localhost:3306/数据库名?charset=utf8mb4"
# 创建异步引擎
DB_Engine = create_async_engine(
ASYNC_DATABASE_URL,
echo=True, # 可选:输出sql日志
pool_size=10, # 设置连接池中保持的持久连接数
max_overflow=20 # 设置连接池允许创建的额外连接数
)
# 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(
bind=DB_Engine, # 绑定数据库引擎
class_= AsyncSession, # 指定会话类
expire_on_commit=False # 会话对象不过期,不重新查询数据库
)
# 依赖项,用于获取数据库会话
async def Get_DB():
async with AsyncSessionLocal() as session:
try:
yield session # 返回数据库会话给路由处理函数
await session.commit() # 无异常提交事务
except Exception:
await session.rollback() # 有异常则回滚
raise
finally:
await session.close() # 关闭会话
查询操作
核心语句:await db.execute( select(模型类) ),返回一个 ORM 对象
查全部
-
scalars().all():@app.get("/book/get_books") async def get_book_list(db: AsyncSession=Depends(get_database)): result = await db.execute(select(Book)) book = result.scalars().all() return book
查单个
-
scalars().first():@app.get("/book/get_book") async def get_book(db: AsyncSession=Depends(get_database)): book=result.scalars().first() return book -
get(模型类, 主键值):@app.get("/book/get_book") async def get_book(db: AsyncSession=Depends(get_database)): book = await db.get(Book, 1) return book
条件查询
语法格式:select(Book).where(条件, 条件2, ...)
条件:
-
比较判断:
==;>;<;>=;<=等app.get("/book/{book_id}") async def get_book_list(book_id: int, db: AsyncSession = Depends(get_database)): result = await db.execute(select(Book).where(Book.id == book_id)) book = result.scalar_one_or_none() return book[!TIP]
scalars():提取标量值-
scalars().all():获取所有结果 -
scalars().first(): 提取第一个数据 -
scalars().one():必须恰好有一个结果 -
scalar_one_or_none(): 提取一个或 null
-
-
模糊查询:
like()app.get("/book/get_books") async def get_book_list(db: AsyncSession = Depends(get_database)): book = result.scalars().all() return book%:零个、一个或多个字符:_:一个单个字符
-
与非查询:
&;|;~@app.get("/book/get_books") async def get_book_list(db: AsyncSession = Depends(get_database)): result = await db.execute(select(Book).where((Book.author == "曹雪芹") & (Book.price == 200))) book = result.scalars().all() return book -
包含查询:
in_()@app.get("/book/get_books") async def get_book_list(db: AsyncSession = Depends(get_database)): id_list = [1, 2, 3, 4, 5, 6] book = result.scalars().all() result = await db.execute(select(Book).where(Book.id.in_(id_list))) return book
聚合查询
聚合计算:func.方法(模型类.属性)
- count:统计行数量
- avg:求平均值
- max:求最大值
- min:求最小值
- sum:求和
app.get("/book/count")
async def get_count(db: AsyncSession = Depends(get_database)):
# result = await db.execute(select(func.count(Book.id)))
# result = await db.execute(select(func.max(Book.price)))
# result = await db.execute(select(func.sum(Book.price)))
result = await db.execute(select(func.avg(Book.price)))
count = result.scalar()
return count
分页查询
分页查询:select().offset().limit()
offset:跳过的记录数limit:返回的记录数
[!TIP]
offset值= (当前页码 - 1) * 每页数量limit
@app.get("/book/get_books")
async def get_book_list(
page: int = 1,
page_size: int = 3,
db: AsyncSession = Depends(get_database)
):
skip = (page-1) * page_size
result = await db.execute(select(Book).offset(skip).limit(page_size))
books = result.scalars().all()
return {"books": books}
联表查询
连表查询的前提是两张表在模型层面已经建立了关联。
假设我们有两个表:User(用户)和 Post(文章),一个用户可以有多篇文章(一对多):
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
# 1. 用户模型 (父表)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
# 【关键】定义关系:一个用户有多个 posts
# back_populates 用于双向绑定,方便互相访问
posts: Mapped[list["Post"]] = relationship(back_populates="owner")
# 2. 文章模型 (子表)
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(100))
content: Mapped[str] = mapped_column(String)
# 【关键】外键:指向 users 表的 id
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
# 【关键】定义关系:一篇文章属于一个 owner (User)
owner: Mapped["User"] = relationship(back_populates="posts")
使用 .join()实现连表查询:
@app.get("/users/who-wrote-python")
def get_users_who_wrote_python(db: Session = Depends(get_db)):
# 显式 Join: 从 User 表 join Post 表
stmt = (
select(models.User)
.join(models.Post) # 默认是 INNER JOIN
.where(models.Post.title.like("%Python%"))
.distinct() # 防止一个用户写多篇 Python 文章导致结果重复
)
result = db.execute(stmt)
users = result.scalars().all()
return [{"name": u.name} for u in users]
新增操作
核心步骤:定义 ORM 对象 → 添加对象到事务:add(对象) → commit 提交到数据库
app.post("/book/add_book")
async def add_book(book: BookBase, db: AsyncSession = Depends(get_database)):
# 创建数据库模型对象
book_obj = Book(**book.__dict__) # 获取 book 参数,创建图书对象(__dict__ 返回 book 对象的属性字典)
# book_obj = Book(**book.dict()) # 推荐使用 .dict() 方法
db.add(book_obj)
await db.commit()
return book
-
__dict__作用:# 假设传入的 book 参数是: # BookBase(title="Python编程", author="张三", price=99.0) print(book.__dict__) # 输出:{'title': 'Python编程', 'author': '张三', 'price': 99.0} -
**解包操作:# 这行代码: book_obj = Book(**book.__dict__) # 等价于: book_obj = Book( title=book.title, author=book.author, price=book.price ) # 或者: book_obj = Book(title="Python编程", author="张三", price=99.0)
缺省异常处理
from sqlalchemy.exc import IntegrityError
@app.post("/book/add_book")
async def add_book(book: BookBase, db: AsyncSession = Depends(get_database)):
try:
book_obj = Book(**book.dict())
db.add(book_obj)
await db.commit()
await db.refresh(book_obj)
return book_obj
#缺省异常
except IntegrityError:
await db.rollback()
return {"error": "书籍添加失败,可能存在重复数据"}
完整实例代码:
from pydantic import BaseModel from sqlalchemy import Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() # Pydantic 模型(用于请求体验证) class BookBase(BaseModel): title: str author: str price: float class Config: schema_extra = { "example": { "title": "Python编程", "author": "张三", "price": 99.0 } } # SQLAlchemy 模型(用于数据库) class Book(Base): __tablename__ = "books" id = Column(Integer, primary_key=True, index=True) title = Column(String, index=True) author = Column(String) price = Column(Float) # FastAPI 端点 @app.post("/book/add_book") async def add_book(book: BookBase, db: AsyncSession = Depends(get_database)): # 将 Pydantic 模型转换为 SQLAlchemy 模型 book_obj = Book(**book.dict()) # 推荐使用 .dict() 而不是 .__dict__ # 添加到数据库会话 db.add(book_obj) # 提交事务到数据库 await db.commit() # 刷新对象,获取数据库生成的值(如自增ID) await db.refresh(book_obj) # 返回添加的书籍信息 return book_obj
更新操作
核心步骤:查询 get → 属性重新赋值 → commit 提交到数据库
@app.put("/book/update_book/{book_id}")
async def update_book(book_id: int, data: BookUpdate, db: AsyncSession = Depends(get_database)):
# 1. 查询
book = await db.get(Book, book_id)
if book is None:
raise HTTPException(status_code=404, detail="数据不存在")
# 2. 修改属性(重新赋值)
book.bookname = data.bookname
book.author = data.author
book.price = data.price
# 3. 提交
await db.commit()
return book
完整示例代码:
from fastapi import FastAPI, HTTPException, Depends from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from typing import Optional # 假设的模型定义 class BookUpdate(BaseModel): bookname: Optional[str] = None author: Optional[str] = None price: Optional[float] = None @app.put("/book/update_book/{book_id}") async def update_book( book_id: int, data: BookUpdate, db: AsyncSession = Depends(get_database) ): # 1. 查询要更新的书籍 book = await db.get(Book, book_id) # 2. 检查书籍是否存在 if book is None: raise HTTPException(status_code=404, detail=f"ID为{book_id}的书籍不存在") # 3. 修改属性(只更新提供的字段) if data.bookname is not None: book.bookname = data.bookname if data.author is not None: book.author = data.author if data.price is not None: book.price = data.price # 4. 提交事务 await db.commit() # 5. 刷新对象获取最新状态(可选) await db.refresh(book) # 6. 返回更新后的书籍信息 return book
删除操作
核心步骤:查询 get → delete 删除 → commit 提交到数据库
@app.delete("/book/delete_book/{book_id}")
async def delete_book(book_id: int, db: AsyncSession = Depends(get_database)):
# 先查询
db_book = await db.get(Book, book_id)
if db_book is None:
raise HTTPException(status_code=404, detail="Book not found")
await db.delete(db_book)
await db.commit()
return {"message": "删除成功"}
完整示例代码:
from fastapi import FastAPI, HTTPException, Depends from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import SQLAlchemyError @app.delete("/book/delete_book/{book_id}") async def delete_book( book_id: int, db: AsyncSession = Depends(get_database) ): # 1. 先查询要删除的书籍 db_book = await db.get(Book, book_id) # 2. 检查书籍是否存在 if db_book is None: raise HTTPException( status_code=404, detail=f"ID为 {book_id} 的书籍不存在" ) try: # 3. 删除书籍 await db.delete(db_book) # 4. 提交事务 await db.commit() # 5. 返回成功信息 return {"message": f"ID为 {book_id} 的书籍已成功删除"} except SQLAlchemyError as e: # 捕获到的异常对象赋值给变量 e # 6. 错误处理 await db.rollback() raise HTTPException( status_code=500, detail=f"删除失败: {str(e)}" )
JWT
加密处理
安装依赖:
# passlib[bcrypt]==1.7.4 官⽅⻓期稳定版本
pip install "passlib[bcrypt]==1.7.4"
创建加密上下文:
from passlib.context import CryptContext
# 创建密码上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 密码加密
def get_hash_password(password: str):
return pwd_context.hash(password)
生成访问令牌
Token:是服务器发给客户端的一段字符串,用来在后续请求中证明"你已经登录过了",作用:解决HTTP是五状态的问题,在每次请求中"自我证明身份"。
token在请求在是的位置是请求头:
Authorization: Bearer<token>
通过生成一个随机的 UUID 字符串作为 Token,并将其存储在数据库的 UserToken 表中,以此来管理用户的登录状态:
# 生成Token
async def create_token(DB: AsyncSession, user_id: int):
# 生成 token +设置过期时间 --> 查询数据库当前用户是否有 token --> 决定是否更新
# 生成随机 token:
token = str(uuid.uuid4())
# 设置过期时间:
expires_at = datetime.now() + timedelta(days=1) # timedelta(days=?,hours=?,minutes=?,seconds=?)
# 查询数据库:检查是否存在旧token
res = await DB.execute(select(UserToken).where(UserToken.user_id == user_id))
user_token = res.scalar_one_or_none()
# 如果token存在就更新 token 和 过期时间,不存在就在数据库添加 token
if user_token:
user_token.token = token
user_token.expires_at = expires_at
else:
user_token = UserToken(user_id=user_id, token=token, expires_at=expires_at)
DB.add(user_token)
await DB.commit()
return token
统一响应格式
在utils/中新增response.py定义通用响应格式方法success_response:
完整代码:
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
def success_response(message: str = 'success', data=None):
content = {
"code": 200,
"message": message,
"data": data
}
# 把任何FastAPI、Pydantic、ORM对象都要正常响应-->code、message、data
return JSONResponse(content=jsonable_encoder(content))
具体步骤:
先定义函数:
def success_response(message: str = 'success', data=None):
message: 默认值为'success'。用于向前端传达操作结果的简要描述(如 "用户创建成功"、"登录成功")。data: 默认为None。用于承载具体的业务数据(如用户信息列表、单个对象、ID 等)。
构建响应内容结构:
content = {
"code": 200,
"message": message,
"data": data
}
返回响应结果:
return JSONResponse(content=jsonable_encoder(content))
jsonable_encoder(content):- 假设
data是一个 SQLAlchemy 的User对象,或者包含datetime字段。 - 这个函数会递归遍历
content字典,把所有非 JSON 原生类型的对象“清洗”成字典或基本类型。 - 如果不加这一步:直接
JSONResponse(content=content)可能会在遇到 ORM 对象或时间对象时抛出TypeError: Object of type ... is not JSON serializable。
- 假设
JSONResponse(...):- 将编码后的字典序列化为 JSON 字符串。
- 生成一个 Starlette/FastAPI 兼容的响应对象。
- 默认 HTTP 状态码为 200 OK。
导入依赖说明:
from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder
JSONResponse: FastAPI 提供的响应类。它不仅能返回字典,还能自动设置正确的Content-Type: application/json头,并允许你自定义 HTTP 状态码(虽然这里主要用它的序列化能力)。jsonable_encoder: 这是核心工具。FastAPI 内置的一个编码器,作用是将 Python 对象(包括 Pydantic 模型、SQLAlchemy ORM 对象、datetime时间对象、Enum枚举等)转换成标准的 JSON 兼容格式(如 dict、list、str、float)。
使用方法:
from utils.response import success_response
success_response(message="注册成功😄", data=responses_data)
异常处理
全局异常处理器
全局异常处理器(Global Exception Handler)是注册在 FastAPI 应用级别的异常处理函数,用于捕获业务层、数据
库层以及系统层抛出的异常,并以统一的响应格式返回给前端。
异常:
- SQL 错误
- 外键关联失败
- 数据库连接异常
- 提交事务失败
在exception.py中编写异常处理:
import traceback
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette import status
# 开发模式:返回详细错误信息
# 生产模式:返回简化错误信息
DEBUG_MODE = True # 教学项目保持开启
# 处理 HTTPException 异常
async def http_exception_handler(request: Request, exc: HTTPException):
# HTTPException 通常是业务逻辑主动抛出的,data 保持 None
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"data": None
}
)
# 处理数据库完整性约束错误
async def integrity_error_handler(request: Request, exc: IntegrityError):
error_msg = str(exc.orig)
# 判断具体的约束错误类型
if "username_UNIQUE" in error_msg or "Duplicate entry" in error_msg:
detail = "用户名已存在"
elif "FOREIGN KEY" in error_msg:
detail = "关联数据不存在"
else:
detail = "数据约束冲突,请检查输入"
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": "IntegrityError",
"error_detail": error_msg,
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"code": 400,
"message": detail,
"data": error_data
}
)
# 处理 SQLAlchemy 数据库错误
async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": type(exc).__name__,
"error_detail": str(exc),
# 格式化异常信息为字符串,方便日志记录和调试
"traceback": traceback.format_exc(),
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"code": 500,
"message": "数据库操作失败,请稍后重试",
"data": error_data
}
)
# 处理所有未捕获的异常
async def general_exception_handler(request: Request, exc: Exception):
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": type(exc).__name__,
"error_detail": str(exc),
# 格式化异常信息为字符串,方便日志记录和调试
"traceback": traceback.format_exc(),
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"code": 500,
"message": "服务器内部错误",
"data": error_data
}
)
在exception_handler.py编写全局异常处理器:
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from utils.exception import http_exception_handler, integrity_error_handler, sqlalchemy_error_handler, \
general_exception_handler
# 注册全局异常处理
def register_exception_handler(app):
# 子类在前,父类在后;具体在前,抽象在后
app.add_exception_handler(HTTPException, http_exception_handler) # 业务
app.add_exception_handler(IntegrityError, integrity_error_handler) # 数据完整性约束
app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) # 数据库
app.add_exception_handler(Exception, general_exception_handler) # 兜底
add_exception_handler是什么?
- 是注册一个自定义的异常处理函数。当应用程序抛出特定类型的异常时,FastAPI 会调用你注册的 handler 函数来处理,而不是直接返回默认的 500 错误。
在main.py中注册异常处理器:
from utils.exception_handler import register_exception_handlers
# 注册异常处理器
register_exception_handlers(app)
数据库缓存
数据缓存:缓存是一种存储机制,用于临时存储数据或计算结果,当再次需要这些数据时,可以快速从缓存中检索,而不是重新
进行耗时或昂贵的获取和计算过程。
在网站开发中,缓存(Cache)是一个非常重要的概念,其核心作用是提高性能、降低延迟和减轻服务器负载。
主要优势:
- 提升性能和用户体验
- 减轻服务器/数据库负载
- 降低网络延迟
- 节省资源和成本
项目中缓存流程:
数据库缓存策略
最常见的策略:旁路策略
- 读:先查缓存,有数据则返回,没有数据则查询数据库
- 写: 更新数据库后,更新或删除缓存数据
[!TIP]
旁路缓存策略:是一种常见的缓存策略,其核心概念是应用程序主动管理缓存,在读取数据时先检查
缓存,如果缓存中没有数据,则从数据库或其他数据源加载数据,并将数据存入缓存;当数据更新或删除时,应用程
序也负责更新或删除缓存中的数据。
不同类型的数据,缓存时间不同,否则会出现缓存雪崩,数据越稳定,缓存越久;数据变化越快,缓存越短:
| 类型 | 时间 |
|---|---|
| 分类、配置 | 7200(2小时) |
| 列表数据 | 600(10分钟) |
| 详情数据 | 1800(30分钟) |
| 验证码 | 120(2分钟) |
Redis安装与配置
Redis 是一种高性能的 Key-Value 存储系统,它将数据存储在内存中,因此读写速度极快,非常适合作为应用层的
缓存服务。在 FastAPI 这样的后端框架中,通常在应用层使用像 Redis 这样的内存数据存储作为缓存。
安装Redis客户端:
pip install redis
配置Redis客户端:
import redis.asyncio as redis
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
# 创建Redis连接对象
redis_client = redis.Redis(
# 配置参数:
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True
)
配置参数:
host:Redis服务器地址port:端口号(默认:6379)db:数据库编号(0~16)decode_responses:是否讲返回的数据从字节流编码转化为字符串
Redis缓存方法
缓存操作就是围绕 Redis 做“存、取、删、判断、过期”等操作,让数据访问更快、数据库压力更小。
Redis 存储数据:key - value
Redis 缓存方法:
setex:设置缓存,并指定过期时间key:str:缓存的键名(例如"user:1001")expire:int: 过期时间,单位为秒value:str:要存储的值(通常是将对象序列化后的 JSON 字符串)
get:获取缓存值,若不存在,这返回Nonekey:str:要查询的键名
delete:删除指定的缓存键key:str:要查询的键名
exists:检查缓存键是否存在,返回布尔值key:str:要查询的键名
封装缓存操作方法
读取缓存
# 获取缓存
async def Get_Cache(key: str):
try:
return await redis_client.get(key)
except Exception as e:
print(f"获取缓存失败: {e}")
return None
# 获取JSON缓存
async def Get_Json_Cache(key: str):
try:
data = await redis_client.get(key)
if data:
return json.loads(data) # 序列化,解析JSON字符串
return None
except Exception as e:
print(f"获取 JSON 缓存失败: {e}")
return None
设置缓存
# 设置缓存
async def Set_Cache(key: str, value: Any, expire: int = 3600):
try:
# 检查是否为字典或列表
if isinstance(value, (dict, list)):
# 序列化:转字符串
value = json.dumps(value, ensure_ascii=False) # 保留中文字符
await redis_client.setex(key, expire, value)
return True
except Exception as e:
print(f"设置缓存失败: {e}")
return False
[!TIP]
⚠️注意:添加Redis数据源中的驱动版本选择
1.5