FastAPI

前言

因为我本身是学习Java的,如果你也是,那么你学习 FastAPI 会非常顺畅!很多概念几乎可以无缝迁移。

让我用熟悉的 Spring Boot 视角来帮你快速建立对应关系:

核心概念对应表(Spring Boot → FastAPI)

Spring Boot 概念 FastAPI 对应概念 相似度
@RestController @app.get()/@app.post() 等路由装饰器 ★★★★★
@RequestMapping 路径参数和装饰器(@app.api_route ★★★★☆
@Autowired Depends() 依赖注入 ★★★★☆
@Component/@Service 普通函数 + Depends() ★★★★☆
@Repository 数据库操作函数/类 ★★★★☆
@Configuration 启动时初始化代码/依赖项 ★★★☆☆
@Bean 使用 lifespan 或单例依赖 ★★★☆☆
Interceptor/Filter 依赖项或中间件 ★★★★☆
@ControllerAdvice 异常处理器(@app.exception_handler ★★★★☆
@Valid 和 DTO Pydantic 模型验证 ★★★★★
application.properties 环境变量或 .env 文件 ★★★★☆
Spring Security 依赖项 + OAuth2 工具 ★★★★☆

你会立即感到熟悉的几个方面

1. 注解式开发 → 装饰器开发

1
2
3
4
# FastAPI(Python装饰器风格)
@app.get("/users/{id}") # ← 就像 @GetMapping
def get_user(id: int, db: Session = Depends(get_db)): # ← 自动注入依赖
return db.query(User).filter(User.id == id).first()

2. 依赖注入的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义一个“服务类”(类似Spring的@Service)
class UserService:
def get_user(self, user_id: int):
# 业务逻辑...
return {"id": user_id, "name": "John"}

# 创建实例(类似@Bean)
user_service = UserService()

# 注入使用(类似@Autowired)
@app.get("/users/{id}")
def read_user(id: int, service: UserService = Depends(lambda: user_service)):
return service.get_user(id)

3. 数据验证(DTO → Pydantic)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 类似Java的DTO类
from pydantic import BaseModel

class UserCreate(BaseModel): # ← 就像你的Java DTO
username: str
email: str
age: int

# 内置验证(类似@NotBlank @Email)
@validator('email')
def validate_email(cls, v):
if '@' not in v:
raise ValueError('Invalid email')
return v

@app.post("/users/")
def create_user(user: UserCreate): # ← 自动验证和转换
# user.username, user.email 已经是验证过的数据
return {"message": "User created"}

需要稍作调整的思维模式

  1. 更函数式,更少“仪式感”

    • 不需要定义接口
    • 不需要 getter/setter
    • 没有复杂的 XML 配置
  2. 更直接的请求-响应

    • 参数直接从函数参数获取
    • 返回值直接就是响应体
    • 异步支持是天生的(async/await
  3. 更灵活的依赖注入

    • 可以是函数,可以是类,都可以注入
    • 可以基于路径参数、查询参数、header 等动态创建依赖

学习建议

  1. 直接找对应关系:每当你想“在 Spring Boot 里我会怎么做”,就去查 FastAPI 的对应方案
  2. 从 Pydantic 开始:这是 FastAPI 的“杀手锏”,类似 Lombok + Validation 的超强组合
  3. 理解 Python 的特性:类型提示、装饰器、async/await - 这些是 FastAPI 强大的基础
  4. 别怕写“简单”代码:Python 代码看起来比 Java 简单,但功能一样强大

快速入门路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Day 1: 基本对应
# Java: @GetMapping("/hello") → FastAPI:
@app.get("/hello")
def hello(name: str = "World"): # 查询参数自动处理
return {"message": f"Hello {name}"}

# Day 2: 依赖注入
# Java: @Autowired UserService → FastAPI:
def get_db(): # 类似 @Bean
db = SessionLocal()
try:
yield db # 类似 @Scope("request")
finally:
db.close()

@app.get("/items")
def get_items(db: Session = Depends(get_db)): # 自动注入
return db.query(Item).all()

# Day 3: 安全验证
# Java: Spring Security → FastAPI:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)):
# 验证逻辑...
return user

@app.get("/users/me")
def read_me(current_user = Depends(get_current_user)):
return current_user

好消息是…

你的 Spring Boot 经验有 80% 可以直接迁移!

  • REST API 设计
  • 分层架构(Controller/Service/Repository)
  • 依赖注入的价值
  • 数据验证的重要性
  • 配置管理
  • 错误处理

FastAPI 只是用更 Pythonic、更简洁的方式实现了相同的理念。你会发现在 FastAPI 中实现同样的功能,代码量可能只有 Spring Boot 的 1/3 到 1/2,但功能和性能一样强大。

我们学习java学的是理念,FastAPI 只是换了种更轻量、更现代的语言和语法来实现它。

下面是我的笔记~

FastAPI 入门 - FastAPI 框架

Python 版本最低3.8 推荐3.10以上 我是用3.13

Python环境

为了适应各种Python版本,这里使用MiniConda 来作为Python环境,MiniConda 可以在创建虚拟环境的时候指定Python 版本号,并且没有Anaconda 那么多乱七八糟的第三方库

一:MiniConda

Miniconda - Anaconda

Download Success - post download | Anaconda

安装完后要将路径添加到环境变量当中

1
2
3
4
5
6
# 根路径
D:\miniconda
# Scripts 路径
D:\miniconda\Scripts
# bin路径
D:\miniconda\Library\bin

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
C:\Users\Lenovo>conda <==== 输入conda
usage: conda-script.py [-h] [-v] [--no-plugins] [-V] COMMAND ...

conda is a tool for managing and deploying applications, environments and packages.

options:
-h, --help Show this help message and exit.
-v, --verbose Can be used multiple times. Once for detailed output, twice for INFO logging, thrice for DEBUG
logging, four times for TRACE logging.
--no-plugins Disable all plugins that are not built into conda.
-V, --version Show the conda version number and exit.

commands:
The following built-in and plugins subcommands are available.

COMMAND
activate Activate a conda environment.
clean Remove unused packages and caches.
commands List all available conda subcommands (including those from plugins). Generally only used by
tab-completion.
compare Compare packages between conda environments.
config Modify configuration values in .condarc.
content-trust Signing and verification tools for Conda
create Create a new conda environment from a list of specified packages.
deactivate Deactivate the current active conda environment.
doctor Display a health report for your environment.
env Create and manage conda environments.
export Export a given environment
info Display information about current conda install.
init Initialize conda for shell interaction.
install Install a list of packages into a specified conda environment.
list List installed packages in a conda environment.
menuinst A subcommand for installing and removing shortcuts via menuinst.
notices Retrieve latest channel notifications.
package Create low-level conda packages. (EXPERIMENTAL)
remove (uninstall) Remove a list of packages from a specified conda environment.
rename Rename an existing environment.
repoquery Advanced search for repodata.
run Run an executable in a conda environment.
search Search for packages and display associated information using the MatchSpec format.
token Set repository access token and configure default_channels
tos A subcommand for viewing, accepting, rejecting, and otherwise interacting with a channel's
Terms of Service (ToS). This plugin periodically checks for updated Terms of Service for the
active/selected channels. Channels with a Terms of Service will need to be accepted or
rejected prior to use. Conda will only allow package installation from channels without a
Terms of Service or with an accepted Terms of Service. Attempting to use a channel with a
rejected Terms of Service will result in an error.
update (upgrade) Update conda packages to the latest compatible version.

C:\Users\Lenovo>

二:虚拟环境

默认情况下,我们直接通过pip install 的方式将包安装在系统级别的Python环境中,这样存在一个问题,就是如果有个老项目用FastAPI是老版本做的,然后你现在想用最新的FastAPI版本来开发新项目,这时候就会碰到一个问题,如何在电脑中同事拥有两套环境?这个时候就是使用我们的虚拟环境来解决这个问题。

首先,我们创建一个基于Python 3.13 版本的虚拟环境:

1
2
# 使用3.13
conda create -n fastapi-env python=3.13

通过下列命令可以管理虚拟环境:

1
2
3
4
5
6
7
8
9
10
11
# 查看所有的虚拟环境
conda env list
# 删除虚拟环境
conda env remove -n [虚拟环境名称]

# 进入虚拟环境
# 首次运行要执行:conda init
conda activate [虚拟环境名称]

# 退出虚拟环境
conda deactivate

三:设置pip源

默认情况下,使用pip命令会从python官网上下载,由于Python官网服务器在国外,下载速度有点慢,我们可以改为国内滴,比如清华,搜索引擎搜索一下也有很多其他的

1
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

结果:

1
2
3
4
C:\Users\Lenovo>conda activate fastapi-env

(fastapi-env) C:\Users\Lenovo>pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
Writing to C:\Users\Lenovo\AppData\Roaming\pip\pip.ini

开发工具

PyCharm

这个不多说啦

我是教育邮箱申请的pro

或者你可以去某宝购买。

Mysql

javaer应该都会的

也可以使用其他的数据库可视化软件

FastAPI 介绍

一:FastAPI 简介

FastAPI 是一个高性能的 Python Web 框架,旨在为开发人员提供快速、简洁且强大的 API 开发体验。它于 2017 年由 Sebastian Ramirez 发布,旨在解决当时市场上现有框架在处理 HTTP 请求和响应时的复杂性和维护困难问题。

Sebastian Ramirez 在开发 FastAPI 时,面临着构建 API 的挑战,他发现现有的框架在处理 HTTP 请求和响应时过于复杂,难以维护。因此,他决定开发一个更简洁、更易于使用的框架,以帮助其他开发人员更专注于业务逻辑,而不是处理 HTTP 请求和响应。

FastAPI 支持异步编程,这意味着它可以更有效地处理并发请求,提高应用程序的性能。此外,FastAPI 还支持多种数据库和数据存储后端,如 PostgreSQL、MySQL、SQLite、MongoDB 等,以及 ORM 框架,如 SQLAlchemy。这使得开发者可以轻松地将 FastAPI 与各种数据库集成,构建强大的数据驱动应用程序。

许多知名的公司和项目已经开始使用 FastAPI,包括 Google、Netflix、Amazon、Dropbox 等。这些公司在使用 FastAPI 时,都对其高性能和简洁性给予了高度评价。

FastAPI 的基准测试成绩也证明了其高性能。根据官方的基准测试结果,FastAPI 在处理并发请求时,其性能优于其他流行的 Web 框架,如 Flask 和 Django。这使得 FastAPI 成为构建高性能 Web 应用程序和 API 的理想选择。

Django、Flask 和 FastAPI 三大框架的特点:

  • 功能:Django > Flask > FastAPI
  • 性能:FastAPI > Flask > Django

二:FastAPI的优点

  • 快速:与NodeJS和Go性能相当,是最快的Python web框架之一。
  • 高效编码:提升功能开发速度200%至300%。
  • 更少bug:减少约40%开发者人为错误。
  • 智能:编辑器自动补全以减少调试时间。
  • 简单:易使用和学习,读文档时间更短。
  • 简短:代码重复最小化,通过参数声明实现丰富功能,bug更少。
  • 健壮:生产可用级别代码,含自动生成的交互式文档。
  • 标准化:基于OpenAPI和JSON Schema。

三:安装

使用下面这个命令即可安装:

1
pip install "fastapi[standard]"

四:一个最简单的FastAPI程序

在Pycharm中选择FastAPI项目新建

当然了记得创建.gitignore 因为ide会自带一些文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from fastapi import FastAPI

app = FastAPI()

# 服务器在定义URL的时候,用了什么method,那么客户端在请求这个URL的时候就要使用相同的method
# GET:从服务器上获取资源
# POST:数据到服务器
# DELETE: 要删除服务器上的数据
# PUT:要求改服务器上的数据

# async
@app.get("/")
async def root():
# 异步函数,协程
# await 访问数据库() 等IO操作

return {"message": "Hello World"}

# /hello/yjy
# /hello/yangjiayu
@app.get("/hello/{name}")
async def say_hello(name: str):
return {"message": f"Hello {name}"}

Uvicorn

uvicorn是一个高性能的异步web服务器。我们也可以使用uvicorn来直接运行FastAPI项目。首先通过以下命令安装uvicorn:

1
pip install "uvicorn[standard]"

然后可以通过下面命令运行项目:

1
uvicorn main:app --reload

测试API

另外我们可以在项目根路径下test_main.http 中测试我们写的API是否有错误

具体项目代码(后续所有代码都放在这里):yjyrichard/fastapi-learning: fastapi学习代码

Pydantic介绍

在其他web框架中,比如Django或Flask,我们通过定义表单类来校验数据。而在FastAPI中,我们使用Pydantic来实现这一功能。

Pydantic的特点:

  • 由类型提示支持:少学习、代码少、与IDE工具集成。
  • 速度快:Rust核心验证,Python最快数据验证库之一。
  • JSON模式:与其他工具集成。
  • 生态系统丰富:PyPI约8000个包,含FastAPI等流行库。
  • Battle测试:月下载超7000万次,被多家大型科技公司使用。

安装

由于FastAPI依赖Pydantic,因此在安装FastAPI时会把pydantic也一起安装了。

基本使用

Pydanyic 是通过定义模型,以及在模型中指定字段来对值进行校验的,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 1. 导入所需的模块
from datetime import date
from typing import List
# 除了BaseModel,我们还需要导入ValidationError来处理验证错误
from pydantic import BaseModel, ValidationError

# 2. 定义数据模型 (与之前相同)
class User(BaseModel):
"""
定义一个名为User的模型,它继承自pydantic.BaseModel。
这个类定义了我们期望的数据结构、类型和约束。
"""
id: int # 必填字段,必须是整数
name: str = 'John Doe' # 可选字段,默认值为'John Doe'
date_joined: date | None # 可选字段,可以是日期或None
departments: List[str] | None # 可选字段,可以是字符串列表或None

# --- 场景一:使用有效数据,验证成功 ---
print("--- 场景一:使用有效数据 ---")
# 准备符合模型要求的外部数据
valid_data = {
'id': 123,
'date_joined': '2023-06-01', # Pydantic会自动将字符串转换为date对象
'departments': ['技术部', '产品部']
}

try:
# 尝试创建User实例
user = User(**valid_data)
print("✅ 数据验证成功!")
print(f"用户ID: {user.id}")
print(f"用户姓名: {user.name}")
print(f"加入日期: {user.date_joined} (类型: {type(user.date_joined)})")
except ValidationError as e:
# 如果数据有效,这个块不会被执行
print("❌ 数据验证失败!")
print(e)

print("\n" + "="*50 + "\n")

# --- 场景二:使用无效数据,捕获ValidationError ---
print("--- 场景二:使用无效数据并捕获错误 ---")
# 准备不符合模型要求的外部数据
invalid_data = {
'id': 'abc', # 错误:id应该是int,但这里是str
'name': 456, # 错误:name应该是str,但这里是int
'date_joined': '2023-13-01', # 错误:这不是一个有效的日期格式
'departments': '不是列表' # 错误:departments应该是List[str],但这里是str
}

try:
# 尝试用无效数据创建User实例
# 这一行会抛出 ValidationError
user_invalid = User(**invalid_data)
print("✅ 数据验证成功!") # 这行代码不会被执行

except ValidationError as e:
# 捕获到ValidationError异常
print("❌ 数据验证失败!捕获到 ValidationError。")

# 打印一个人类可读的错误摘要
print("\n--- 错误摘要 ---")
print(e)

# 打印详细的错误列表(推荐用于程序化处理)
# e.errors() 返回一个包含所有错误详细信息的字典列表
print("\n--- 详细错误列表 ---")
error_list = e.errors()
for i, error in enumerate(error_list, 1):
print(f"错误 {i}:")
# 'loc' 字段指明了错误发生的位置(字段名)
print(f" - 位置: {' -> '.join(map(str, error['loc']))}")
# 'msg' 字段提供了人类可读的错误信息
print(f" - 信息: {error['msg']}")
# 'type' 字段是机器可读的错误类型标识符
print(f" - 类型: {error['type']}")
print("-" * 20)

请求数据

请求数据包括有路由参数,Body参数,以下分别进行讲解:

这个跟java没有什么区别 直接来看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 导入FastAPI及相关类型和工具
from fastapi import FastAPI, Body, Query, Path, Form, File, UploadFile
from pydantic import BaseModel, Field
from typing import Annotated

# 创建FastAPI应用实例
app = FastAPI()

# --- 1. 路径参数 ---
# 路径参数是URL路径的一部分,用于标识特定资源。
@app.get("/items/{item_id}")
async def read_item(item_id: int):
# FastAPI会自动将URL中的item_id转换为int类型
# 如果传入的不是整数,会返回一个清晰的验证错误
return {"item_id": item_id}

# --- 2. 查询参数 ---
# 查询参数是URL中 `?` 后面的键值对,用于过滤或修改请求。
@app.get("/users/")
async def read_users(q: Annotated[str | None, Query(max_length=50)] = None):
# q是可选的查询参数,如果没有提供,默认为None
# 使用Query可以添加额外的验证,如最大长度
if q:
return {"query": q}
return {"message": "No query parameter provided"}

# --- 3. 请求体 - Pydantic 模型 ---
# 请求体通常用于POST、PUT等请求,通过JSON格式传递复杂的数据结构。
# 首先定义一个Pydantic模型用于数据验证和序列化
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None

@app.post("/items/")
async def create_item(item: Item):
# 声明item参数的类型为Item模型
# FastAPI会读取请求体,验证数据,并将其转换为Item实例
return item

# --- 4. 混合使用路径、查询和请求体参数 ---
# 一个操作中可以同时包含多种类型的参数。
@app.put("/items/{item_id}")
async def update_item(
item_id: Annotated[int, Path(title="The ID of the item to update", ge=1)],
q: str | None = None,
item: Annotated[Item, Body(embed=True)]
):
# item_id: 路径参数,使用Path添加了元数据和验证(必须大于等于1)
# q: 查询参数
# item: 请求体参数,Body(embed=True)表示JSON需要有一个键"item"来包裹数据
results = {"item_id": item_id, "item": item}
if q:
results["q"] = q
return results

# --- 5. 表单数据 ---
# 当Content-Type为 `application/x-www-form-urlencoded` 时使用,常用于HTML表单提交。
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
# 使用Form来声明表单字段
return {"username": username}

# --- 6. 文件上传 ---
# 当Content-Type为 `multipart/form-data` 时使用,用于上传文件。
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 使用UploadFile来接收文件,它比直接使用bytes更节省内存
# file.filename: 获取文件名
# file.content_type: 获取文件类型
# await file.read(): 异步读取文件内容
return {
"filename": file.filename,
"content_type": file.content_type
}

# --- 7. 文件和表单字段混合 ---
# 可以在同一个请求中同时接收文件和表单字段。
@app.post("/files/")
async def create_file(
file: bytes = File(), description: str = Form(...)
):
# 使用bytes = File()会将文件内容全部读入内存
# 使用Form接收其他表单字段
# 适用于小文件
return {
"file_size": len(file),
"description": description
}

依赖注入

依赖注入可以让我们的视图函数在执行之前先执行一段逻辑代码,这段逻辑代码可以返回新的值给视图函数。在以下场景中可以使用依赖注入:

  • 共享业务逻辑(复用相同的代码逻辑)
  • 共享数据库连接
  • 实现安全、验证、角色权限
  • 等等。

总之,就是将一些重复性的代码单独写成依赖,然后在需要的视图函数中注入这个依赖。

因为我之前是学习Java的 这样解释我反而有点看不太懂

跟spring的核心理念是相同的意思是:

“控制反转(IoC)”和“依赖倒置(DIP)”:将对象的创建和管理权交给框架,使用者只声明“我需要什么”,框架负责“提供什么”。

springboot这样写嘛:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserService {
public User getUser(Long id) {
// ... 业务逻辑
}
}

@RestController
public class UserController {
@Autowired // 核心:将业务Bean注入控制器
private UserService userService;

@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}

fastapi这样写:

1
2
3
4
5
6
7
8
9
10
# 1. 声明一个依赖项(如权限检查)
def get_current_user(token: str = Depends(oauth2_scheme)):
# ... 验证token,返回用户信息
return user

# 2. 在视图函数中注入这个依赖
@app.get("/users/me")
def read_users_me(current_user: User = Depends(get_current_user)):
# 框架会自动先执行 get_current_user,将其结果注入给 current_user
return current_user

总之:就是别总是 “new ”啦 找框架要吧。

将一些重复性的代码单独写成以来,任何在需要的视图函数中,注入这个依赖。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 依赖注入
from fastapi import Depends
from typing import Dict

async def page_common(page: int=0,size: int=10):
return {"page": page,"size": size}

@app.get("/user/list")
async def user_list(page_params: Dict=Depends(page_common)):
page = page_params["page"]
size = page_params["size"]
return {"page": page,"size": size}

APIRouter

在FastAPI项目中,我们不可能把所有视图都放到main.py 文件中,这时候久需要将视图进行分类,任何同类的放到一个单独的子路由下。在FastAPI 中可以使用 APIRouter 类进行实现。【好比 java里面的 [业务名称]Controller 这样看起来更加清晰嘛】

新建一个包:routers

里面新建两个py文件分别为:article.pyuser.py

user.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from fastapi import APIRouter

router = APIRouter(prefix="/user", tags=["user"])

# /user/list
@router.get("/list")
async def user_list():
return {"users" : ["zs","ls"]}

# /user/123
@router.get('/{user_id}')
async def user_detail(user_id):
return {"user_id" : user_id}

article.py 也差不多 就不粘贴了

现在还没有跟main.py进行关联

1
2
3
4
5
6
from fastapi import FastAPI
from routers.user import router as user_router
from routers.article import router as article_router #避免覆盖
app = FastAPI()
app.include_router(user_router)
app.include_router(article_router)

这个配置方式让我感觉跟vue很像

想象成组装一台电脑:

  • FastAPI/Vue 方式(模块化组装):

    1
    2
    3
    4
    1. 买一个空机箱(app = FastAPI() / createApp())
    2. 买独立显卡(user_router)
    3. 买独立内存条(article_router)
    4. 组装起来(app.include_router())

    灵活:需要什么装什么

  • Spring Boot 方式(一体化方案):

    1
    2
    3
    1. 买品牌整机(@SpringBootApplication)
    2. 开机即用(自动扫描所有组件)
    3. 配置都在说明书里(application.properties)

    省心:开箱即用,功能齐全

学习迁移建议

  1. FastAPI 的 appVue 的 appSpring Boot 的启动类
    • 都是应用的核心容器
    • 都负责全局配置
    • 都是模块注册的中心
  2. FastAPI 的 APIRouterVue 的组件Spring Boot 的 @Controller
    • 都是功能模块
    • 都可以独立开发和测试
    • 都可以被主应用"挂载"
  3. 最大的区别
    • FastAPI/Vue: 显式注册(你要手动 include_router
    • Spring Boot: 隐式注册(自动扫描 @Controller

现代框架都在向 “核心应用 + 模块化插件” 的模式发展。这种模式让代码更清晰、更易维护,也让学习曲线更平滑——学会一个,其他的就很容易理解了。

app = FastAPI() 就是 FastAPI 世界的 “Spring Boot Application”,是整个应用的启动器和配置中心。**

数据库连接和模型

在Python框架中,我们通常会使用ORM(Object Relationship Mapping)框架来操作数据库。目前主流的ORM框架有:SQLALchemy、Peewee ORM、Pony ORM、GINO、Tortoise ORM、orman等。而SQLALchemy是功能最强大,且社区最活跃的ORM库,并且还支持异步,因此我选择用SQLALchemy作为操作数据库的ORM框架。

另外,FastAPI作者开发了一个ORM框架叫做SQLModel,但是这个框架目前来说还不完善,文档都还处于开发期间,它底层也是基于SQLALchemy Core来实现的。SQLAlchemy文档地址为:https://www.sqlalchemy.org/

安装

1.安装sqlalchemy

通过以下命令安装:

1
pip install "sqlalchemy[asyncio]"

将会安装异步版本的sqlalchemy

然后再执行以下命令安装aiomysql 驱动:

1
2
3
# 同步:mysqlclient
# 异步:aimysql
pip install aiomysql

2.安装cryptography

使用Python连接MySQL需要用cryptography对密码进行加密,所以还需要安装以下包:

1
pip install cryptography

创建连接

配置连接参数

使用SQLAlchemy 连接数据库,是通过设置一个固定格式的字符串来实现的。mysql+aiomysql 为例,那么其连接格式如下:

1
DB_URL = "mysql+aiomysql://用户名:密码@主机名:端口号/数据库名称?charset=utf8mb4"

示例如下:

1
DB_URL = "mysql+aiomysql://root:root@127.0.0.1:3306/bookdb?charset=utf8mb4"

创建Engine 对象

SQLAlchemy 中的 Engine 对象,它负责管理数据库连接的创建(并不直接操作数据库),连接池的维护,SQL语句的翻译等。Engine对象在整个程序中只能有一个。创建Engine对象的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine(
DB_URI,
# 将输出所有执行SQL的日志(默认是关闭的) 生产环境关掉
echo=True,
# 连接池大小(默认是5个)
pool_size=10,
# 允许连接池最大的连接数(默认是10个)
max_overflow=20,
# 获得连接超时时间(默认是30s)
pool_timeout=10,
# 连接回收时间(默认是-1,代表永不回收)
pool_recycle=3600,
# 连接前是否预检查(默认为False)
pool_pre_ping=True,
)

创建会话工厂

使用 sqlalchemy_orm.sessionmarker 类来创建会话工厂,这个会话工厂实际就是Session 或者它的子类,以后如果要操作数据库,那么就需要创建一个会话工厂的对象 (即:Session类的对象),来完成相关操作。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession

AsyncSessionFactory = sessionmaker(
# Engine或者其子类对象(这里是AsyncEngine)
bind=engine,
# Session类的代替(默认是Session类)
class_=AsyncSession,
# 是否在查找之前执行flush操作(默认是True)
autoflush=True,
# 是否在执行commit操作后Session就过期(默认是True)
expire_on_commit=False
)

创建模型

定义Base 类

Base 类是所有ORM Model 类的父类,一个ORM Model 类对应数据库中的一张表。ORM Model 中的一个Column 类属性对应数据库表中的一个字段。Base类的生成可以使用 sqlalchemy.ext.declarative.declarative_base 函数来实现,也可以继承 sqlalchemy.MetaData 类,实现自己的子类,并在子类中编写约束规范。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import MetaData

# 定义命名约定的Base类
class Base(DeclarativeBase):
metadata = MetaData(naming_conventions={
# ix: index, 索引。
"ix": 'ix_x(column_0_label)s',
# un: unique, 唯一约束
"uq": "uq_x(table_name)s_x(column_0_name)s",
# ck: Check, 检查约束
"ck": "ck_x(table_name)s_x(constraint_name)s",
# fk: Foreign Key, 外键约束
"fk": "fk_x(table_name)s_x(column_0_name)s_x(referred_table_name)s",
# pk: Primary Key, 主键约束
"pk": "pk_x(table_name)s"
})

创建ORM模型

这里我们以创建一个User模型为例来说明模型的创建:

1
2
3
4
5
6
7
8
9
10
11
from typing import Optional
from sqlalchemy import Integer, String, select
from sqlalchemy.orm import Mapped, mapped_column

class User(Base):
__tablename__ = 'user'

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
username: Mapped[str] = mapped_column(String(100))
password: Mapped[str] = mapped_column(String(200))

模型关系

这里我们使用外键来实现一对一,一对多和多对多,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""
用户相关模型 - SQLAlchemy ORM 关系映射示例

=== 对比 MyBatis/MyBatis-Plus 的核心区别 ===

1. MyBatis: 需要手写 XML 或注解来定义 SQL 和结果映射
MyBatis-Plus: 通过继承 BaseMapper 自动生成单表 CRUD
SQLAlchemy: 通过 Python 类定义,自动生成 SQL,关联查询也自动处理

2. MyBatis: resultMap 手动配置关联查询 (association/collection)
SQLAlchemy: relationship() 自动处理关联加载,不需要写 XML

3. MyBatis-Plus: @TableName, @TableId, @TableField 注解
SQLAlchemy: __tablename__, mapped_column() 定义
"""

from typing import List, Optional, TYPE_CHECKING
from . import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import Integer, String, ForeignKey

if TYPE_CHECKING:
from .article import Article


class User(Base):
"""
用户表 - 演示一对一、一对多关系

=== MyBatis-Plus 对比 ===

@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;

private String email;
private String username;
private String password;

// MyBatis-Plus 不直接支持关联映射
// 需要手动查询或使用 XML 配置
@TableField(exist = false)
private UserExtension userExtension;

@TableField(exist = false)
private List<Article> articles;
}

=== MyBatis XML 关联查询对比 ===

<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="email" column="email"/>
<result property="username" column="username"/>
<!-- 一对一关联 -->
<association property="userExtension"
javaType="UserExtension"
column="id"
select="selectUserExtensionByUserId"/>
<!-- 一对多关联 -->
<collection property="articles"
ofType="Article"
column="id"
select="selectArticlesByAuthorId"/>
</resultMap>
"""
__tablename__ = 'user' # 对比 MyBatis-Plus: @TableName("user")

# ============ 基础字段 ============
# 对比 MyBatis-Plus: @TableId(type = IdType.AUTO)
# 对比 MyBatis XML: <id property="id" column="id"/>
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

# 对比 MyBatis-Plus: 默认驼峰转下划线,或 @TableField("email")
# index=True 会创建索引,MyBatis 需要在建表 SQL 中单独写
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
password: Mapped[str] = mapped_column(String(200))

# ============ 一对一关系: User -> UserExtension ============
"""
【一对一关系详解】

MyBatis 写法:
<association property="userExtension"
javaType="UserExtension"
column="id"
select="selectUserExtensionByUserId"/>

// 或者嵌套结果映射
<association property="userExtension" javaType="UserExtension">
<id property="id" column="ext_id"/>
<result property="university" column="university"/>
</association>

MyBatis-Plus 写法:
// MP 不支持自动关联,需要手动查询
User user = userMapper.selectById(1);
UserExtension ext = userExtensionMapper.selectOne(
new QueryWrapper<UserExtension>().eq("user_id", user.getId())
);
user.setUserExtension(ext);

SQLAlchemy 写法:
relationship() 自动处理!访问 user.user_extension 时自动查询

关键点:
- 外键在 UserExtension 表中 (user_id)
- back_populates 实现双向绑定
- uselist=False 表示一对一(返回单个对象,不是列表)
"""
user_extension: Mapped[Optional["UserExtension"]] = relationship(
"UserExtension", # 关联的类名
back_populates="user", # 对方类中反向引用的属性名
uselist=False, # False = 一对一,True = 一对多
cascade="all, delete-orphan" # 级联删除,删User时自动删UserExtension
)

# ============ 一对多关系: User -> Articles ============
"""
【一对多关系详解】

MyBatis 写法:
<collection property="articles"
ofType="Article"
column="id"
select="selectArticlesByAuthorId"/>

// 或者嵌套结果映射
<collection property="articles" ofType="Article">
<id property="id" column="article_id"/>
<result property="title" column="title"/>
</collection>

MyBatis-Plus 写法:
// MP 不支持自动关联,需要手动查询
User user = userMapper.selectById(1);
List<Article> articles = articleMapper.selectList(
new QueryWrapper<Article>().eq("author_id", user.getId())
);
user.setArticles(articles);

SQLAlchemy 写法:
relationship() 自动处理!访问 user.articles 时自动查询

关键点:
- 外键在 Article 表中 (author_id)
- List 类型表明这是"一"的一方,持有"多"的集合
- lazy 控制加载时机,类似 MyBatis 的懒加载配置
"""
articles: Mapped[List["Article"]] = relationship(
"Article", # 关联的类名
back_populates="author", # Article 类中反向引用的属性名
lazy="selectin" # 加载策略
# lazy 选项:
# - "select": 懒加载,访问时单独查询(默认,类似 MyBatis 延迟加载)
# - "selectin": 使用 IN 查询批量加载(推荐,解决 N+1 问题)
# - "joined": 使用 JOIN 立即加载(类似 MyBatis 嵌套结果映射)
# - "subquery": 使用子查询加载
)


class UserExtension(Base):
"""
用户扩展信息表 - 演示一对一关系的"从"方(持有外键的一方)

=== MyBatis-Plus 对比 ===

@TableName("user_extension")
public class UserExtension {
@TableId(type = IdType.AUTO)
private Long id;

private String university;
private Long userId; // 外键字段

@TableField(exist = false) // 非数据库字段
private User user;
}

=== MyBatis XML 对比 ===

<resultMap id="UserExtensionResultMap" type="UserExtension">
<id property="id" column="id"/>
<result property="university" column="university"/>
<result property="userId" column="user_id"/>
<association property="user"
javaType="User"
column="user_id"
select="selectUserById"/>
</resultMap>
"""
__tablename__ = 'user_extension'

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
university: Mapped[str] = mapped_column(String(100))

# ============ 外键字段 ============
"""
外键定义 - 这是关系的"物理连接"

MyBatis-Plus: 就是普通字段 private Long userId;
MyBatis XML: column="user_id" 在 association 中指定

SQLAlchemy 区别:
- ForeignKey('user.id'): 数据库层面的外键约束
- relationship(): ORM 层面的对象关联
- 两者分开定义,更清晰

unique=True 保证一对一(一个 user_id 只能出现一次)
"""
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey('user.id'), # 指向 user 表的 id 字段
unique=True # 一对一关系需要唯一约束!
)

# ============ 一对一关系: UserExtension -> User ============
"""
【反向关联】

MyBatis: 通过 association + select 实现
SQLAlchemy: relationship + back_populates 实现双向绑定

back_populates vs backref:
- back_populates: 两边都要写 relationship(推荐,更清晰)
- backref: 只在一边写,自动在另一边创建属性(简洁但隐式)
"""
user: Mapped["User"] = relationship(
"User",
back_populates="user_extension"
)


# ============================================================
# SQLAlchemy vs MyBatis/MyBatis-Plus 核心对比总结
# ============================================================
"""
【查询方式对比】

1. 单表查询:
MyBatis-Plus: userMapper.selectById(1)
SQLAlchemy: session.get(User, 1)

2. 条件查询:
MyBatis-Plus: userMapper.selectList(new QueryWrapper<User>().eq("email", "test@test.com"))
SQLAlchemy: session.execute(select(User).where(User.email == "test@test.com"))

3. 关联查询(最大区别!):
MyBatis-Plus: 需要手动写两次查询,再 set 进去
MyBatis XML: 需要配置 association/collection
SQLAlchemy: 直接访问 user.articles,自动查询!

【为什么 SQLAlchemy 更方便?】

MyBatis/MyBatis-Plus 的关联查询:
User user = userMapper.selectById(1);
// 还要再查一次!
List<Article> articles = articleMapper.selectList(
new QueryWrapper<Article>().eq("author_id", 1)
);
user.setArticles(articles);

SQLAlchemy 的关联查询:
user = session.get(User, 1)
# 直接用!SQLAlchemy 自动处理关联查询
for article in user.articles:
print(article.title)
"""

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
"""
文章模型 - 演示多对一关系 (ManyToOne) 和多对多关系 (ManyToMany)

=== 关系概览 ===
- Article -> User: 多对一 (多篇文章属于一个作者)
- Article <-> Tag: 多对多 (一篇文章可以有多个标签,一个标签可以标记多篇文章)
"""

from typing import List, TYPE_CHECKING
from . import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import Integer, String, ForeignKey, Text, Table, Column

# TYPE_CHECKING: 仅在类型检查时导入,运行时不导入
if TYPE_CHECKING:
from .user import User


# ============================================================
# 多对多关系的中间表 (Association Table)
# ============================================================
"""
【多对多关系 - 中间表定义】

这是 SQLAlchemy 处理多对多关系的方式,需要一个中间表。

=== Java/JPA 对比 ===

@Entity
public class Article {
@ManyToMany
@JoinTable(
name = "article_tag",
joinColumns = @JoinColumn(name = "article_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags;
}

JPA 中使用 @JoinTable 注解,SQLAlchemy 需要显式定义 Table 对象。

=== MyBatis 对比 ===

MyBatis 需要手写中间表的查询:

<resultMap id="ArticleWithTagsMap" type="Article">
<id property="id" column="id"/>
<collection property="tags" ofType="Tag">
<id property="id" column="tag_id"/>
<result property="name" column="tag_name"/>
</collection>
</resultMap>

<select id="selectArticleWithTags" resultMap="ArticleWithTagsMap">
SELECT a.*, t.id as tag_id, t.name as tag_name
FROM article a
LEFT JOIN article_tag at ON a.id = at.article_id
LEFT JOIN tag t ON at.tag_id = t.id
WHERE a.id = #{id}
</select>
"""
# 中间表:只存储两个外键,没有额外字段时用 Table
# 如果中间表需要额外字段(如创建时间),则需要定义为完整的 Model 类
article_tag = Table(
'article_tag', # 表名
Base.metadata, # 元数据对象
Column('article_id', Integer, ForeignKey('article.id'), primary_key=True),
Column('tag_id', Integer, ForeignKey('tag.id'), primary_key=True)
)


class Article(Base):
"""
文章表 - 演示多对一关系的"多"方,以及多对多关系

=== Java/JPA 对比 ===

@Entity
@Table(name = "article")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(length = 100)
private String title;

@Lob // 大文本
private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private User author;

@ManyToMany
@JoinTable(name = "article_tag", ...)
private List<Tag> tags;
}

=== MyBatis 对比 ===

<resultMap id="ArticleResultMap" type="Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<!-- 多对一关联 -->
<association property="author"
column="author_id"
javaType="User"
select="selectUserById"/>
<!-- 多对多关联 -->
<collection property="tags"
column="id"
ofType="Tag"
select="selectTagsByArticleId"/>
</resultMap>
"""
__tablename__ = 'article'

# ============ 基础字段 ============
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(100))

# Text 类型对应数据库的 TEXT/LONGTEXT
# 类似 JPA 的 @Lob 注解
content: Mapped[str] = mapped_column(Text)

# ============ 外键字段 ============
"""
外键定义 - 多对一关系中,外键在"多"的一方

JPA: @JoinColumn(name = "author_id")
MyBatis: 在查询中通过 column="author_id" 关联
"""
author_id: Mapped[int] = mapped_column(
Integer,
ForeignKey('user.id') # 指向 user 表的 id
)

# ============ 多对一关系: Article -> User ============
"""
【多对一关系详解】

SQLAlchemy 写法:
Mapped["User"] (单个对象,不是 List) 表示这是"多对一"中的"多"方

JPA 写法:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private User author;

MyBatis 写法:
<association property="author"
column="author_id"
select="selectUserById"/>

关键点:
- 外键 author_id 在本表中
- Mapped["User"] 是单个对象,表示"多对一"
- 对比 User 中的 Mapped[List["Article"]],那是"一对多"
"""
author: Mapped["User"] = relationship(
"User", # 关联的类名
back_populates="articles" # User 类中反向引用的属性名
)

# ============ 多对多关系: Article <-> Tag ============
"""
【多对多关系详解】

SQLAlchemy 写法:
使用 secondary 参数指定中间表

JPA 写法:
@ManyToMany
@JoinTable(
name = "article_tag",
joinColumns = @JoinColumn(name = "article_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags;

MyBatis 写法:
需要手写关联查询,通过中间表 JOIN

关键点:
- 多对多需要中间表 (article_tag)
- 两边都是 List 类型
- secondary 参数指定中间表
- 双向多对多,两边都需要定义 relationship
"""
tags: Mapped[List["Tag"]] = relationship(
"Tag", # 关联的类名
secondary=article_tag, # 中间表!这是多对多的关键
back_populates="articles" # Tag 类中反向引用的属性名
)


class Tag(Base):
"""
标签表 - 演示多对多关系

=== Java/JPA 对比 ===

@Entity
@Table(name = "tag")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String name;

@ManyToMany(mappedBy = "tags") // 被动方使用 mappedBy
private List<Article> articles;
}

=== MyBatis 对比 ===

<resultMap id="TagResultMap" type="Tag">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="articles"
column="id"
ofType="Article"
select="selectArticlesByTagId"/>
</resultMap>
"""
__tablename__ = 'tag'

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), unique=True)

# ============ 多对多关系: Tag <-> Article ============
"""
【多对多 - 双向定义】

多对多关系两边都要定义 relationship,使用相同的 secondary 中间表

JPA 中:
- 一方使用 @JoinTable 定义(主动方)
- 另一方使用 mappedBy(被动方)

SQLAlchemy 中:
- 两边都使用 secondary 指定同一个中间表
- back_populates 建立双向关联
"""
articles: Mapped[List["Article"]] = relationship(
"Article",
secondary=article_tag, # 使用相同的中间表!
back_populates="tags"
)


# ============================================================
# 补充: 带额外字段的多对多关系 (Association Object Pattern)
# ============================================================
"""
【高级用法 - 中间表有额外字段】

如果中间表需要存储额外信息(如:用户收藏文章的时间),
就不能用简单的 Table,需要定义完整的 Model 类。

=== 场景示例 ===
用户收藏文章,需要记录收藏时间

=== Java/JPA 对比 ===

// 需要创建一个中间实体
@Entity
public class UserFavorite {
@EmbeddedId
private UserFavoriteId id;

@ManyToOne
@MapsId("userId")
private User user;

@ManyToOne
@MapsId("articleId")
private Article article;

@Column(name = "created_at")
private LocalDateTime createdAt;
}

=== SQLAlchemy 写法 ===

from datetime import datetime

class UserFavorite(Base):
__tablename__ = 'user_favorite'

user_id: Mapped[int] = mapped_column(ForeignKey('user.id'), primary_key=True)
article_id: Mapped[int] = mapped_column(ForeignKey('article.id'), primary_key=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)

# 关联
user: Mapped["User"] = relationship(back_populates="favorites")
article: Mapped["Article"] = relationship(back_populates="favorited_by")

class User(Base):
favorites: Mapped[List["UserFavorite"]] = relationship(back_populates="user")

class Article(Base):
favorited_by: Mapped[List["UserFavorite"]] = relationship(back_populates="article")

这种模式叫做 Association Object Pattern,
类似 JPA 中把多对多拆成两个一对多的做法。
"""

迁移模型

模型定义好后,要将模型映射到数据库中生成表,或者以后模型上的字段名,字段类型等发生改变了,可以非常方便的使用 alembic 来进行迁移

安装alembic

通过下列命令来安装alembic:

1
pip install alembic==1.13.2

迁移

创建迁移仓库

alembic 的使用类似git,也可以进行版本回退,并且都需要先创建好一个迁移仓库。在项目根路径下,使用以下命令生成仓库:

1
alembic init alembic --template async

如果项目Mysql驱动不是异步欸都,比如pymysql,那么就不需要执行 --template async

修改alembic.ini

要将模型迁移到仓库中,还需要修改alembic.ini 下连接数据库的配置,修改代码如下:

1
2
# 注释sqlalchemy.url
# sqlalchemy.url=

修改env.py

将alembic/env.py文件夹中的target_metadata修改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import settings
from models import Base

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# 添加连接数据库的配置
database_url = settings.DB_URI
if database_url is None:
raise ValueError("No database URL provided")
config.set_main_option("sqlalchemy.url", database_url)


# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata

生成迁移脚本

如果模型发生改变了,那么需要先将模型生成迁移脚本,执行以下命令:

1
alembic revision --autogenerate -m "修改的内容"

这样就会在alembic/versions 下生成迁移脚本文件。

执行迁移脚本

在alembic/versions 下生成迁移脚本后,模型的修改并没有同步到数据库中,因此还需要执行以下命令:

1
alembic upgrade head

MyBatis 是以 SQL 为中心(写 SQL -> 映射结果),而 SQLAlchemy 是以对象为中心(操作对象 -> 自动生成 SQL)。

如果想要回到上一次的版本,那么可以使用以下命令来实现:

1
2
3
4
5
6
7
8
# 回退到上一个版本 (undo last migration)
alembic downgrade -1

# 或者回退到指定版本号
# alembic downgrade <revision_id>

# 回退到最初始状态 (删除所有表)
alembic downgrade base

CRUD 操作 (增删改查)

配置好模型和数据库连接后,接下来就是实际的业务操作。这里我们使用异步会话 (AsyncSession) 来演示。

核心思维转换

  • MyBatis: 你调用 Mapper 接口的方法,MyBatis 帮你执行绑定的 SQL。
  • SQLAlchemy: 你操作 Python 对象(创建对象、修改属性),然后告诉 Session “提交”,SQLAlchemy 帮你翻译成 SQL 并执行。

1. 新增 (Create)

在 SQLAlchemy 中,新增数据就是创建一个对象,然后 add 到会话中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from models import User, UserExtension

async def create_user_demo(session: AsyncSession):
"""
新增用户

=== MyBatis 对比 ===
User user = new User();
user.setUsername("admin");
userMapper.insert(user); // 此时数据库已有数据

=== SQLAlchemy 逻辑 ===
1. 实例化对象 (内存中)
2. session.add (加入到事务暂存区,数据库还没提交)
3. session.commit (提交事务,真正写入数据库)
4. session.refresh (可选,为了拿回数据库生成的自增 ID 或默认值)
"""

# 1. 创建主表对象
new_user = User(
username="admin",
email="admin@example.com",
password="hashed_password_123"
)

# 2. 创建关联表对象 (一对一)
# SQLAlchemy 的强大之处:可以直接赋值对象,不用手动拿 new_user.id 赋给 extension
user_ext = UserExtension(university="Peking University")
new_user.user_extension = user_ext

# 3. 添加到会话
session.add(new_user)
# 因为建立了关联关系,add(new_user) 会级联 add(user_ext),也可以手动 add

try:
# 4. 提交事务
await session.commit()

# 5. 刷新对象(重新从数据库加载,为了获取自增 id)
await session.refresh(new_user)
print(f"用户创建成功,ID: {new_user.id}")

except Exception as e:
await session.rollback() # 异常回滚
print(f"创建失败: {e}")

2. 查询 (Read)

这是与 MyBatis 差异最大的地方。SQLAlchemy 2.0 统一使用 select() 构建查询语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from sqlalchemy import select
from sqlalchemy.orm import selectinload

async def get_user_demo(session: AsyncSession):
"""
查询用户

=== MyBatis 对比 ===
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", "admin"));

=== SQLAlchemy 逻辑 ===
构建 select 语句 -> 执行(execute) -> 获取结果(scalars)
"""

# 1. 根据主键 ID 查询 (最简单)
# 类似 MyBatis: selectById(1)
user = await session.get(User, 1)
if user:
print(f"找到用户: {user.username}")

# 2. 条件查询
# 类似 MyBatis: selectList(new QueryWrapper().eq("username", "admin"))
stmt = select(User).where(User.username == "admin")
result = await session.execute(stmt)

# scalars().first() 获取单个对象
# scalars().all() 获取对象列表
user_obj = result.scalars().first()

# 3. 关联查询 (解决 N+1 问题)
# MyBatis: 需要配置 <resultMap> 或 <association> 并在 XML 写 JOIN
# SQLAlchemy: 使用 options(selectinload(User.articles))

stmt = (
select(User)
.where(User.id == 1)
.options(
# 预加载 articles (一对多) 和 user_extension (一对一)
selectinload(User.articles),
selectinload(User.user_extension)
)
)

result = await session.execute(stmt)
u = result.scalars().first()

if u:
# 此时访问 u.articles 不需要再次查询数据库,因为已经预加载了
print(f"用户文章数: {len(u.articles)}")
print(f"用户学校: {u.user_extension.university if u.user_extension else '无'}")

3. 更新 (Update)

SQLAlchemy 有两种更新方式。

方式一:对象操作(推荐,符合 ORM 直觉)
先查出来,改属性,再提交。SQLAlchemy 会自动监测哪些字段变了(Dirty Checking)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def update_user_orm_style(session: AsyncSession):
"""
先查后改

=== MyBatis 对比 ===
User user = userMapper.selectById(1);
user.setEmail("new@test.com");
userMapper.updateById(user);
"""
# 1. 查
user = await session.get(User, 1)
if user:
# 2. 改 (内存操作)
user.email = "new_email@example.com"

# 3. 提交 (自动生成 UPDATE 语句)
await session.commit()

方式二:批量更新(类似 SQL UPDATE 语句)
适合批量操作,不需要先把对象查到内存里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sqlalchemy import update

async def update_user_sql_style(session: AsyncSession):
"""
直接构建 Update 语句

=== MyBatis 对比 ===
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", 1).set("email", "direct@test.com");
userMapper.update(null, updateWrapper);
"""
stmt = (
update(User)
.where(User.id == 1)
.values(email="direct_update@example.com")
)
await session.execute(stmt)
await session.commit()

4. 删除 (Delete)

同样有两种方式。

方式一:对象操作

1
2
3
4
5
6
7
8
async def delete_user_orm_style(session: AsyncSession):
# 1. 查
user = await session.get(User, 1)
if user:
# 2. 标记删除
await session.delete(user)
# 3. 提交
await session.commit()

方式二:批量删除

1
2
3
4
5
6
7
8
9
10
from sqlalchemy import delete

async def delete_user_sql_style(session: AsyncSession):
"""
=== MyBatis 对比 ===
userMapper.delete(new QueryWrapper<User>().eq("id", 1));
"""
stmt = delete(User).where(User.id == 1)
await session.execute(stmt)
await session.commit()

总结:MyBatis vs SQLAlchemy 转习惯

为了让你更顺滑地过渡,请记住以下几点:

  1. Session 是核心:MyBatis 里你注入的是 UserMapper,SQLAlchemy 里你注入的是 AsyncSession。Session 是所有操作的入口。
  2. Explicit vs Implicit
    • MyBatis 很明确:你调用 select 它就 select,你调用 update 它就 update
    • SQLAlchemy ORM 模式下:你修改了对象的属性,必须显式调用 session.commit(),它才会根据差异生成 UPDATE 语句。
  3. Await 一切:因为你选用了 asyncio,记得所有的 session.execute, session.commit, session.get 等涉及 I/O 的操作前都要加 await
  4. 关于 Relationship:这是上面模型代码里写得最好的部分。在 MyBatis 里处理关联表(比如查用户带出文章列表)通常很痛苦(写 XML 嵌套查询)。在 SQLAlchemy 里,只要模型定义好了 relationship,查询时加上 .options(selectinload(...)) 就可以自动组装数据,这是 ORM 最大的优势。

如果你有后端(Python/Java)背景,理解前端框架会非常快。uni-app 的核心思想是**“一套代码,多端运行”**,这就好比 Java 的“一次编写,到处运行”,uni-app 将 Vue 代码编译成 iOS、Android、H5 以及各种小程序(微信、支付宝等)的原生代码。


uni-app 快速入门讲义

一、uni-app 介绍

1.1 什么是 uni-app?

uni-app 是一个使用 Vue.js 开发所有前端应用的框架。开发者编写一套代码,可发布到 iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。

1.2 核心优势

  • 跨平台:真正的“一套代码,多端发行”。
  • 学习成本低:基于通用的前端技术栈(Vue 语法 + 微信小程序 API)。只要懂 Vue,基本上就学会了 uni-app。
  • 生态丰富:拥有插件市场,有大量现成的组件和模板。
  • 性能优秀:App 端基于 weex/nvue 渲染,性能接近原生。

1.3 技术栈对比(给后端开发者的类比)

  • HTML/模板:在 uni-app 中是 <template>,类似 Python 的 Jinja2 或 Java 的 JSP/Thymeleaf。
  • CSS/样式:在 uni-app 中是 <style>,支持 SCSS/LESS。
  • JS/逻辑:在 uni-app 中是 <script>,处理业务逻辑。
  • API:uni-app 将各端(如微信小程序和浏览器)的差异封装成了统一的 uni.xxx 方法(例如 uni.request 发送 HTTP 请求)。

二、开发环境搭建 (HBuilderX)

虽然 VS Code 也能开发 uni-app,但官方推荐使用 HBuilderX。它内置了 uni-app 的编译器、调试环境和代码提示,是目前开发 uni-app 效率最高的工具。

2.1 安装步骤

  1. 下载:访问 DCloud 官网 下载 HBuilderX。
    • 注意:请下载 App开发版(内置了相关环境),不要下载标准版。
  2. 安装:Windows 解压即可使用;Mac 拖入应用程序。
  3. 注册 DCloud 账号:首次启动需要注册一个账号,用于后续发包和云端打包。

2.2 创建第一个项目

  1. 打开 HBuilderX。
  2. 点击菜单栏 文件 -> 新建 -> 项目
  3. 选择 uni-app 类型。
  4. 输入项目名称(例如 hello-uni)。
  5. 模板选择 “默认模板”(适合从零开始),或者选择 “Hello uni-app”(官方示例,包含大量组件演示,推荐新手看一看)。
  6. Vue 版本选择:推荐 Vue 3(主流),但老项目可能是 Vue 2。
  7. 点击 创建

2.3 运行项目

  • 运行到浏览器:菜单栏 运行 -> 运行到浏览器 -> Chrome
  • 运行到小程序:需要先安装微信开发者工具,并在 HBuilderX 设置中配置微信开发者工具的路径。
  • 运行到手机:连接手机,开启 USB 调试,选择 运行到手机或模拟器

三、项目结构介绍

uni-app 的目录结构非常规范,类似于 Spring Boot 或 Django 的约定大于配置。

3.1 目录树示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─ components            // 自定义组件目录(类似后端封装的公共类/工具类)
│ └─ my-comp
│ └─ my-comp.vue
├─ pages // 页面存放目录(类似于 View 层或 Controller 对应的页面)
│ ├─ index
│ │ └─ index.vue // index页面
│ └─ login
│ └─ login.vue // 登录页面
├─ static // 静态资源目录(图片、字体等,不参与编译)
├─ unpackage // 编译后的包存放目录(类似 target 或 build 目录,不要手动改)
├─ App.vue // 应用配置,用来配置App全局样式以及监听 应用生命周期
├─ main.js // Vue初始化入口文件(类似 main.py 或 SpringBootApplication 类)
├─ manifest.json // 配置应用名称、appid、Logo、权限等打包信息
├─ pages.json // 【重要】全局路由配置、导航栏、底部 TabBar 配置
└─ uni.scss // 全局 SCSS 变量文件

3.2 核心文件详解

1. pages.json (路由与外观配置)

这是 uni-app 中最重要的配置文件。它决定了应用由哪些页面组成,以及窗口的外观。

  • pages 数组:注册应用中所有的页面。如果你写了一个新页面但没在这里注册,它就无法访问(类似 Django 的 urls.py)。
  • globalStyle:定义导航栏颜色、标题文字颜色等全局样式。
  • tabBar:配置底部的 Tab 切换栏(如微信底部的“首页”、“我的”)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#F8F8F8"
}
}

2. App.vue (应用入口组件)

这不是一个页面,而是应用的根组件

  • 应用生命周期
    • onLaunch: 应用初始化完成时触发(全局只触发一次)。常用于检查登录状态、获取全局配置。
    • onShow: 应用启动,或从后台进入前台显示时触发。
    • onHide: 应用从前台进入后台时触发。
  • 全局样式:在 <style> 标签中写的样式,对所有页面生效。

3. main.js (程序入口)

初始化 Vue 实例,引入全局插件。

  • 类似 Java 的 main 方法或 Python 的 if __name__ == '__main__':

4. manifest.json (发布配置)

图形化配置界面。

  • 配置 App 的名称、版本号。
  • 配置 App 的图标(自动生成不同尺寸)。
  • 配置微信小程序的 AppID。
  • 配置各种 SDK(地图、支付、登录)的 Key。

5. .vue 页面文件

标准的 Vue 单文件组件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<!-- 视图层:HTML 代码,只能有一个根节点 -->
<div class="content">
<text>{{ title }}</text>
<button @click="sayHello">点击我</button>
</div>
</template>

<script>
// 逻辑层:JS/TS 代码
export default {
data() {
return {
title: 'Hello uni-app'
}
},
onLoad() {
// 页面生命周期:页面加载时触发(类似 Vue 的 created/mounted)
console.log('页面加载了');
// 这里通常调用 API 获取后端数据
},
methods: {
sayHello() {
uni.showToast({ title: '你好!' })
}
}
}
</script>

<style>
/* 样式层:CSS 代码 */
.content {
padding: 20px;
}
</style>

四、后端开发者快速上手建议

  1. 关于 API 请求
    uni-app 提供了 uni.request,用法类似 Python 的 requests 或 jQuery 的 ajax

    1
    2
    3
    4
    5
    6
    7
    uni.request({
    url: 'http://127.0.0.1:8000/api/users',
    method: 'GET',
    success: (res) => {
    console.log(res.data);
    }
    });

    注意:小程序开发需要配置服务器域名白名单,且必须是 HTTPS(开发环境可以在详情里勾选“不校验合法域名”)。

  2. 关于数据绑定
    Vue 是双向绑定(MVVM)。修改 JS 中的变量 (this.title = '新标题'),界面会自动更新。不需要像操作 DOM 那样手动去改 HTML。

  3. 关于单位
    uni-app 推荐使用 rpx (responsive pixel) 作为 CSS 单位。

    • 规定屏幕宽为 750rpx。
    • 在 iPhone6/7/8 上,1px = 2rpx。
    • 使用 rpx 可以自动适配不同宽度的手机屏幕。

core: 程序的一些核心模块:比如智能体,授权,发送邮件模块等。

models: SQLAlchemy 的ORM模型

repository:操作数据库的逻辑层

routers: APIRouter 分层模型

settings : 项目配置模块 ,可以按照dev pro进行区分

dependencies: FastAPI 项目的依赖项

main: 程序入口文件

schemas: Pydantic 模型

ORM模型