【LLM开发实验】FastAPI 学习笔记

目录

[1. FastAPI 是什么](#1. FastAPI 是什么)

[1.1 三大基石(组合拳)](#1.1 三大基石(组合拳))

[1.2 与 Flask 对比](#1.2 与 Flask 对比)

[1.3 最简示例 + 自动文档](#1.3 最简示例 + 自动文档)

[1.4 核心特性速览](#1.4 核心特性速览)

[2. 路由分发机制(核心)](#2. 路由分发机制(核心))

[2.1 路由本质:装饰器在"登记",不是执行](#2.1 路由本质:装饰器在"登记",不是执行)

[2.2 一条路由能配置什么](#2.2 一条路由能配置什么)

[2.3 匹配流程:方法 → 路径 → 参数绑定](#2.3 匹配流程:方法 → 路径 → 参数绑定)

[2.4 APIRouter 模块化分发(多文件项目关键)](#2.4 APIRouter 模块化分发(多文件项目关键))

[2.5 include_router 总控台](#2.5 include_router 总控台)

[2.6 匹配顺序的坑(重点)](#2.6 匹配顺序的坑(重点))

[2.7 路由分发全流程图](#2.7 路由分发全流程图)

[3. 路径参数 & 查询参数](#3. 路径参数 & 查询参数)

[3.1 路径参数:声明即校验](#3.1 路径参数:声明即校验)

[3.2 路径参数 vs 查询参数](#3.2 路径参数 vs 查询参数)

[4. 增删改查 CRUD](#4. 增删改查 CRUD)

[4.1 内存版完整示例(可运行)](#4.1 内存版完整示例(可运行))

[4.2 细节要点](#4.2 细节要点)

[5. 连接 MySQL(SQLAlchemy 集成)](#5. 连接 MySQL(SQLAlchemy 集成))

[5.1 核心答案:路径不变,变的只是函数内部](#5.1 核心答案:路径不变,变的只是函数内部)

[5.2 一次请求的完整链路(GET /books/3 为例)](#5.2 一次请求的完整链路(GET /books/3 为例))

[5.3 完整可跑示例(单文件版,SQLAlchemy 2.x)](#5.3 完整可跑示例(单文件版,SQLAlchemy 2.x))

[5.4 常见查询模式 → SQL 对照](#5.4 常见查询模式 → SQL 对照)

[5.5 三个最关键机制](#5.5 三个最关键机制)

[5.6 分层项目结构(真实项目标准长法)](#5.6 分层项目结构(真实项目标准长法))

[6. SQLAlchemy 核心机制](#6. SQLAlchemy 核心机制)

[6.1 一句话 + 为什么需要](#6.1 一句话 + 为什么需要)

[6.2 两层架构](#6.2 两层架构)

[6.3 四大核心组件(类比:银行柜台办业务)](#6.3 四大核心组件(类比:银行柜台办业务))

[6.4 一次查询的幕后全流程](#6.4 一次查询的幕后全流程)

[6.5 relationship 一对多](#6.5 relationship 一对多)

[6.6 必踩的坑清单](#6.6 必踩的坑清单)

[7. 实战项目解析:04_sqlalchemy(部门/员工)](#7. 实战项目解析:04_sqlalchemy(部门/员工))

[7.1 文件地图](#7.1 文件地图)

[7.2 base.py ------ 所有模型的"祖先"](#7.2 base.py —— 所有模型的"祖先")

[7.3 database.py ------ 连接配置中心](#7.3 database.py —— 连接配置中心)

[7.4 models.py ------ 手写模型(一对多经典写法)](#7.4 models.py —— 手写模型(一对多经典写法))

[7.5 main.py ------ CRUD 综合演示(老版 query API)](#7.5 main.py —— CRUD 综合演示(老版 query API))

[7.6 gen.py ------ 逆向工程 + 插数据](#7.6 gen.py —— 逆向工程 + 插数据)

[7.7 table_2_models.py ------ 自动生成的模型(2.x 风格)](#7.7 table_2_models.py —— 自动生成的模型(2.x 风格))

[7.8 fastapi_sqlalchemy.py ------ FastAPI 集成](#7.8 fastapi_sqlalchemy.py —— FastAPI 集成)

[7.9 坑与注意(务必记住)](#7.9 坑与注意(务必记住))

[8. 部署:uvicorn 与 nginx](#8. 部署:uvicorn 与 nginx)

[8.1 两者关系:前台 + 后厨(不是二选一)](#8.1 两者关系:前台 + 后厨(不是二选一))

[8.2 生产环境典型架构](#8.2 生产环境典型架构)

[8.3 为什么需要 nginx(uvicorn 的短板)](#8.3 为什么需要 nginx(uvicorn 的短板))

[8.4 路由分发的两层分工(重点辨析)](#8.4 路由分发的两层分工(重点辨析))

[8.5 生产细节](#8.5 生产细节)

[9. 附录:报错排查](#9. 附录:报错排查)

[9.1 ModuleNotFoundError: No module named 'routerss'(案例)](#9.1 ModuleNotFoundError: No module named ‘routerss’(案例))


1. FastAPI 是什么

现代、高性能的 Python Web 框架,用于构建 API。 2018 年由 Sebastián Ramírez 开发,基于 Starlette + Pydantic。

1.1 三大基石(组合拳)

组件 负责部分
Starlette Web 部分:请求路由、并发(性能接近 NodeJS/Go)
Pydantic 数据校验:用 Python 类型注解自动校验请求/响应
类型注解 自动生成 API 文档 + 编辑器自动补全

1.2 与 Flask 对比

对比项 Flask FastAPI
并发模型 同步为主 原生 async 异步
参数校验 手动校验 类型注解自动校验
API 文档 需另装 flasgger 自动生成 Swagger UI (/docs)
性能 一般 高(基于 Starlette)
Python 版本 Python 2/3 仅 Python 3.8+

1.3 最简示例 + 自动文档

复制代码
from fastapi import FastAPI
app = FastAPI()

@app.get("/hello")
def hello(name: str = "world"):
    return {"message": f"Hello {name}"}

启动:uvicorn main:app --reload

两个自动文档(可在线调试):

  • http://127.0.0.1:8000/docs --- Swagger UI
  • http://127.0.0.1:8000/redoc --- Redoc

1.4 核心特性速览

  • 路径参数/users/{user_id},类型不匹配自动返回 422
  • 查询参数:函数参数写默认值即自动识别
  • 请求体 :声明 Pydantic 模型 class Item(BaseModel),自动校验+转文档
  • 依赖注入Depends(),做鉴权/数据库会话等,可复用可测试
  • 异步支持async def 处理高并发 I/O(连数据库、调大模型 API 都合适)

学习路线关联:LLM 应用落地(LangChain 智能体、RAG 查询封装成服务)几乎都用 FastAPI 做接口层,它也是 LangServe、很多 Agent 框架的默认后端。


2. 路由分发机制(核心)

2.1 路由本质:装饰器在"登记",不是执行

复制代码
@app.get("/books/{book_id}")
def get_book(book_id: int):
    ...

这行代码在 import 时就执行了,干两件事:

  1. ("GET", "/books/{book_id}") 登记进 app 的路由表
  2. 绑定处理函数 get_book

函数体此刻【不执行】。 只有将来请求真的来了,才按登记表找到它执行。 FastAPI 也叫"声明式":你声明规则,框架负责匹配。

贯穿类比:路由分发 = 公司前台的分单系统

每个 @app.get 装饰器 = 前台登记册上的一行:"收到 X 类型单子,找张三";请求进来 → 前台查登记册 → 按方法+路径匹配 → 把单子(参数)递给对应的人(函数)。

2.2 一条路由能配置什么

复制代码
@app.post(
    "/books",                          # ① 路径
    status_code=201,                   # ② 成功时返回的状态码
    response_model=BookOut,            # ③ 响应模型:过滤/校验返回数据
    tags=["图书"],                      # ④ 分组标签:/docs 文档按组显示
    summary="新增图书",                 # ⑤ 文档里的说明
    dependencies=[Depends(verify_token)]  # ⑥ 整条路由的前置依赖
)
def create_book(data: BookIn): ...

HTTP 方法对应语义:

方法 语义
@app.get
@app.post
@app.put 整体改
@app.patch 局部改
@app.delete
@app.websocket WebSocket 连接(实时通信)

2.3 匹配流程:方法 → 路径 → 参数绑定

请求进来后 FastAPI 依次做三件事:

复制代码
第1步 方法匹配    POST /books 不会命中 @app.get("/books")
第2步 路径匹配    /books/3 按顺序试路由表:
                 /books          ✗ 没匹配上(还多一段)
                 /books/{id}     ✓ 匹配,把 3 提取出来
第3步 参数绑定    路径里的 3 → 函数参数 book_id:int
                 ?page=2 → page:int=2
                 请求体 JSON → data: BookIn(Pydantic 校验)
                 类型不对直接 422,函数都不执行

一个请求最终"分发"成函数参数的三个来源:

来源 URL 示例 函数写法
路径参数 /books/3 book_id: int
查询参数 /books?page=2 page: int = 2
请求体(JSON) POST 的 body data: BookIn

2.4 APIRouter 模块化分发(多文件项目关键)

真实项目不可能把所有路由堆在 main.py。APIRouter 是"可独立携带的路由表",最后统一挂到主应用:

复制代码
# routers/books.py ------ 图书相关接口都放这
from fastapi import APIRouter

router = APIRouter(prefix="/books", tags=["图书"])

@router.get("")                  # 实际路径 = /books
def list_books(): ...

@router.get("/{book_id}")        # 实际路径 = /books/{book_id}
def get_book(book_id: int): ...

@router.post("")
def create_book(): ...

# routers/users.py ------ 用户相关
user_router = APIRouter(prefix="/users", tags=["用户"])

@user_router.get("/{user_id}")
def get_user(user_id: int): ...

# main.py ------ 汇总挂载,只做"拼装"
from fastapi import FastAPI
from routers import books, users

app = FastAPI()
app.include_router(books.router)     # 挂图书模块
app.include_router(users.router)     # 挂用户模块

每个模块只管自己的 prefix 内部相对路径,前缀一改整个模块路径全变。

2.5 include_router 总控台

复制代码
app.include_router(
    books.router,
    prefix="/api",                   # 全局加前缀:/api/books
    tags=["接口"],                    # 覆盖/追加文档分组
    dependencies=[Depends(verify_token)],  # 整个模块都要登录
)

路由分发分层总表:

层级 位置 管什么
主应用 main.py 挂哪些模块、全局前缀、全局依赖
模块 routers/xxx.py 一类业务的相对路径、自己的依赖
单条路由 @router.get(...) 具体接口 + 参数 + 响应模型
函数参数 def f(...) 把请求内容分发给业务代码

真实项目目录标准长法:

复制代码
project/
├── main.py               # FastAPI() + include_router 汇总
├── routers/
│   ├── books.py          # 图书相关 APIRouter
│   ├── users.py          # 用户相关 APIRouter
│   └── auth.py           # 登录注册 APIRouter
├── models.py             # SQLAlchemy 表模型
├── schemas.py            # Pydantic 模型
└── database.py           # 引擎 + get_db

2.6 匹配顺序的坑(重点)

FastAPI 按 include 的顺序从上往下匹配,先声明先命中:

复制代码
@app.get("/books/latest")     # ✅ 固定路径必须放前面
def latest(): ...

@app.get("/books/{book_id}")  # 放后面
def get_book(book_id): ...

若反了:/books/latest 会被 {book_id} 吞掉 → int 转换失败 422 或直接 404。

2.7 路由分发全流程图

复制代码
请求 POST /api/books/3?debug=1
  │
  ▼ include_router 的 prefix="/api" 剥掉一层
POST /books/3
  │
  ▼ 模块路由表按序匹配 → 命中 router.post("/{book_id}")? 或 get?
  → 方法对不上就 405/404,对上就继续
  │
  ▼ 参数绑定
book_id=3(路径)  debug=1(查询)  body→Pydantic 模型
  │
  ▼ 执行函数 → 返回 → 序列化 JSON 响应

3. 路径参数 & 查询参数

3.1 路径参数:声明即校验

URL 里"会变的那一段",用 {花括号} 占位。

复制代码
@app.get("/books/{book_id}")        # {book_id} 就是路径参数
def get_book(book_id: int):         # 函数参数同名,类型写 int
    return {"你查的是": book_id}

# GET /books/42   → book_id = 42
# GET /books/abc  → 自动 422,类型不匹配

URL 里的值天生是字符串,FastAPI 根据类型注解自动转换:

注解 传 /books/42 结果
book_id: int 字符串 "42" 整数 42(能做数学运算)
book_id: str 保持 "42" 字符串
book_id: float --- 支持小数
不写注解 --- FastAPI 报错,必须写

多个路径参数(天然支持层级路由):

复制代码
@app.get("/users/{user_id}/books/{book_id}")
def get_user_book(user_id: int, book_id: int):
    ...
# GET /users/7/books/3 → user_id=7, book_id=3

⚠ 参数按位置对应,不是按名字,位置错位数据就全乱了。

3.2 路径参数 vs 查询参数

对比项 路径参数 查询参数
URL 长相 /books/3 /books?id=3
位置 URL 路径里 ? 号后面
写法 {book_id} 函数参数带默认值
声明 def f(book_id: int) def f(id: int = 3)
语义 定位"哪一个资源" 附加"筛选/条件"
典型场景 /books/3 拿第3本 /books?page=2&size=10 分页

实际项目混用:

复制代码
@app.get("/books/{book_id}/reviews")
def reviews(book_id: int, page: int = 1):
    # 路径参数定位:哪本书的评论;查询参数:第几页
    ...

4. 增删改查 CRUD

CRUD 是业务系统的地基。类比"图书馆管书":

操作 HTTP方法 URL 语义 典型状态码
POST /books 创建新资源 201 Created
GET /books 查全部列表 200 OK
GET /books/{id} 查单个资源 200 OK / 404
PUT /books/{id} 整体替换 200 OK
PATCH /books/{id} 局部修改 200 OK
DELETE /books/{id} 删除资源 200/204

口诀:POST 提交新东西、GET 拿、PUT/PATCH 改、DELETE 删。 GET 永远不改数据,其他三个都会动数据。

4.1 内存版完整示例(可运行)

复制代码
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# ---------- 数据模型 ----------
class Book(BaseModel):              # 请求/响应 的"图纸"
    title: str
    author: str

# ---------- 模拟数据库(真实项目换成 MySQL/PostgreSQL) ----------
books = []                          # 存所有书
next_id = 1                         # 自增主键

# ---------- 增 Create ----------
@app.post("/books", status_code=201)
def create_book(book: Book):
    global next_id
    item = {"id": next_id, **book.model_dump()}
    books.append(item)
    next_id += 1
    return item

# ---------- 查 Read ----------
@app.get("/books")                  # 查全部
def list_books():
    return books

@app.get("/books/{book_id}")        # 查单个
def get_book(book_id: int):
    for b in books:
        if b["id"] == book_id:
            return b
    raise HTTPException(status_code=404, detail="书不存在")

# ---------- 改 Update ----------
@app.put("/books/{book_id}")        # 整体替换
def update_book(book_id: int, book: Book):
    for i, b in enumerate(books):
        if b["id"] == book_id:
            books[i] = {"id": book_id, **book.model_dump()}
            return books[i]
    raise HTTPException(status_code=404, detail="书不存在")

# ---------- 删 Delete ----------
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    for i, b in enumerate(books):
        if b["id"] == book_id:
            books.pop(i)
            return                  # 204 不需要返回体
    raise HTTPException(status_code=404, detail="书不存在")

4.2 细节要点

  • status_code=201 / 204:增成功返回 201(不是 200),删成功返回 204(无内容)
  • POST vs PUT :POST 每次创建新资源(同一个请求发两次 = 两条记录);PUT 覆盖指定 id 的资源(发两次结果一样 = 幂等
  • 404 是"找不到"的通用答案:查不到/改不到/删不到都抛 404 + detail
  • 路径参数声明成 int :传 /books/abc 自动 422,不用自己写判断
  • 每个操作的三段套路:① 取参数(路径/请求体)② 找数据 ③ 给结果(找到就操作;找不到 raise 404)

5. 连接 MySQL(SQLAlchemy 集成)

5.1 核心答案:路径不变,变的只是函数内部

路径(URL)长什么样,跟用不用 MySQL 没有任何关系。 路径只负责"定位资源",数据存哪、怎么查,全在函数体内部。

接口(对外不变) 底层数据库操作
GET /books SELECT 全部
GET /books/3 SELECT WHERE id=3
GET /books?author=刘慈欣&page=2 条件筛选 + 分页
POST /books INSERT
PUT /books/3 UPDATE
DELETE /books/3 DELETE

5.2 一次请求的完整链路(GET /books/3 为例)

复制代码
1. 浏览器发请求  GET /books/3
2. uvicorn 接收,交给 FastAPI 路由匹配 → 命中 get_book(book_id=3)
3. FastAPI 先执行 Depends(get_db) → 从连接池拿一个数据库会话 db
4. 函数体里 SQLAlchemy 把代码翻译成 SQL:
      SELECT * FROM books WHERE id = 3
   (经 pymysql 驱动真正发给 MySQL 执行)
5. MySQL 返回结果 → SQLAlchemy 包装成 Book 对象
6. FastAPI 把对象转成 JSON 返回给浏览器
7. finally 里关闭会话,连接还给连接池

类比餐厅:URL 是菜单菜名,路由函数是厨师,数据库会话是传菜口,SQLAlchemy 是翻译(不用自己搬货),MySQL 是仓库。换仓库(MySQL→PostgreSQL)只换连接串,菜单一字不用改。

依赖安装:

复制代码
pip install fastapi uvicorn sqlalchemy pymysql
# pymysql 是 MySQL 驱动,SQLAlchemy 是 ORM

5.3 完整可跑示例(单文件版,SQLAlchemy 2.x)

复制代码
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker, DeclarativeBase, Mapped, mapped_column

# ---------- 第1步:连 MySQL ----------
DATABASE_URL = "mysql+pymysql://root:你的密码@localhost:3306/bookdb?charset=utf8mb4"
# 没装 MySQL 想先跑通?换成:sqlite:///./books.db  其他代码一字不改
engine = create_engine(DATABASE_URL, echo=True)   # echo=True 控制台打印SQL
SessionLocal = sessionmaker(bind=engine)

# ---------- 第2步:定义"表"的模型 ----------
class Base(DeclarativeBase):
    pass

class Book(Base):
    __tablename__ = "books"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    author: Mapped[str]

Base.metadata.create_all(engine)      # 没有表就自动建表

# ---------- 第3步:依赖注入 = 每个请求自动拿会话、用完自动关 ----------
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()                    # 防连接泄漏

# ---------- 第4步:接口 ----------
app = FastAPI()

class BookIn(BaseModel):
    title: str
    author: str

# 查全部:SELECT * FROM books
@app.get("/books")
def list_books(db=Depends(get_db)):
    return db.scalars(select(Book)).all()

# 查单个:SELECT * FROM books WHERE id = 3
@app.get("/books/{book_id}")
def get_book(book_id: int, db=Depends(get_db)):
    book = db.get(Book, book_id)
    if not book:
        raise HTTPException(404, "书不存在")
    return book

# 新增:INSERT INTO books (title, author) VALUES (...)
@app.post("/books", status_code=201)
def create_book(data: BookIn, db=Depends(get_db)):
    book = Book(**data.model_dump())
    db.add(book)
    db.commit()                       # 提交事务,真正写进 MySQL
    db.refresh(book)                  # 拿回数据库生成的自增 id
    return book

# 修改:UPDATE books SET ... WHERE id = 3
@app.put("/books/{book_id}")
def update_book(book_id: int, data: BookIn, db=Depends(get_db)):
    book = db.get(Book, book_id)
    if not book:
        raise HTTPException(404, "书不存在")
    book.title = data.title           # 直接改对象属性
    book.author = data.author
    db.commit()
    return book

# 删除:DELETE FROM books WHERE id = 3
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int, db=Depends(get_db)):
    book = db.get(Book, book_id)
    if not book:
        raise HTTPException(404, "书不存在")
    db.delete(book)
    db.commit()

启动后打开 /docs 逐个接口试,控制台会同步打印每条 SQL。

5.4 常见查询模式 → SQL 对照

ORM 代码 生成的 SQL
db.scalars(select(Book)) SELECT * FROM books
db.scalars(select(Book).where(Book.author == "刘慈欣")) ... WHERE author='刘慈欣'
db.scalars(select(Book).where(Book.title.like("%三体%"))) ... WHERE title LIKE '%三体%'
db.scalars(select(Book).offset(10).limit(10)) ... LIMIT 10 OFFSET 10(第2页)
db.get(Book, 3) ... WHERE id=3 只取一条

5.5 三个最关键机制

  1. Depends(get_db) 依赖注入:每个请求自动开会话、结束自动关。千万别在函数里手动 SessionLocal() 还不关------连接会耗尽。会话 ≈ 一次"事务边界",提交才算数。
  2. ORM 翻译 SQL,路由函数里没有一句 SQL 字符串db.get → WHERE、db.add+commit → INSERT、改属性+commit → UPDATE。
  3. 增删改必须 commit,查不用:忘了 commit = 数据没写进去(新手第一大坑);commit 后报错要 rollback,否则锁住连接。

5.6 分层项目结构(真实项目标准长法)

复制代码
database.py   引擎 + 会话工厂 + get_db
models.py     表模型(一张表一个类)
schemas.py    Pydantic 请求/响应模型
main.py       路由(只写业务逻辑,不碰 SQL)
crud.py       数据库操作函数(可选)

6. SQLAlchemy 核心机制

6.1 一句话 + 为什么需要

SQLAlchemy = 用 Python 对象和代码操作数据库,而不是手写 SQL 字符串。Python 世界最流行的 ORM(对象关系映射)库。

手写 SQL 的痛点 SQLAlchemy 的解法
SQL 是字符串,拼错只在运行时报 代码有语法检查,IDE 能补全
数据库不同语法略不同 换库只改连接串
查询结果是裸元组 结果直接是 Python 对象,obj.字段
拼接条件容易 SQL 注入 参数自动转义
表结构改动要同步改很多 SQL 只改模型类一处

ORM 三个字母拆开:Object(对象)↔ Relational(关系表)↔ Mapping(映射)

数据库概念 ORM 概念
一张表 一个 Python 类
一行数据 一个类的实例对象
表之间的外键 relationship 属性

类比图纸和实物:class Book(Base) = 书柜图纸;book = Book(...) = 按图纸造出的实物;db.add(book)+commit = 把实物摆进仓库。

6.2 两层架构

复制代码
┌─────────────────────────────────────┐
│  ORM 层(高层):模型类、会话、对象   │  ← 平时主要用这层
├─────────────────────────────────────┤
│  Core 层(底层):SQL 表达式语言      │  ← 生成标准 SQL
│  Engine:连接池 + 方言(Dialect)       │
├─────────────────────────────────────┤
│  数据库驱动:pymysql / psycopg2...   │  ← 真正发网络请求
└─────────────────────────────────────┘
                  ▼
       MySQL / PostgreSQL / SQLite...

ORM 不是魔法,最终也生成标准 SQL 发给数据库,只是把"写 SQL"替你做了。

6.3 四大核心组件(类比:银行柜台办业务)

1) Engine ------ 自来水厂(连接基础设施)

复制代码
engine = create_engine("mysql+pymysql://root:密码@localhost:3306/bookdb")
  • 连接池:预先维护一批连接,用完回收复用(建连接很贵)
  • 方言:按数据库类型翻译 SQL 语法

2) Session ------ 柜台窗口(★核心,工作单元 Unit of Work)

复制代码
db = Session()          # 开一个窗口
book = Book(...)        # 填单子(内存里构造对象)
db.add(book)            # 单子放上窗口(还没生效!)
db.commit()             # 盖章确认 → 才真正写入数据库
db.close()              # 窗口关闭,资源归还

连接 vs 会话是两回事:

  • 连接(connection):到数据库的物理管道(从 Engine 池子里借的)
  • 会话(session):一次逻辑业务边界,内部按需借用连接

Session 三大纪律(银行柜台规则):

  1. 增删改必须 commit 才生效(忘了 = 单子没盖章)
  2. 反悔用 rollback(一笔业务全部撤销)
  3. 用完必须 close(不关 = 连接泄漏)

3) Model ------ 表结构图纸(声明式映射)

复制代码
class Book(Base):
    __tablename__ = "books"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    author: Mapped[str]

类的每个属性 = 表的一列。Base.metadata.create_all(engine) 按图纸自动建表。生产用迁移工具 Alembic。

4) Query ------ 查询单

复制代码
# 2.x 新版(推荐)
db.scalars(select(Book))
db.scalars(select(Book).where(Book.author == "刘慈欣"))
db.get(Book, 3)

# 1.x 老版(老教程常见,功能相同)
db.query(Book).all()
db.query(Book).filter(Book.author == "刘慈欣").all()
db.query(Book).get(3)

6.4 一次查询的幕后全流程

复制代码
你的代码                           幕后发生的事
db = SessionLocal()                从连接池借一条连接
db.get(Book, 3)                    ORM 翻译 SQL:
                                   SELECT * FROM books WHERE id=3
(拿到 book 对象)                  pymysql 发请求 → MySQL 执行
                                   结果行 → 包装成 Book 实例
book.title                         直接点属性拿值
db.close()                         连接归还连接池

6.5 relationship 一对多

复制代码
class Author(Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    books: Mapped[list["Book"]] = relationship(back_populates="author")

class Book(Base):
    ...
    author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
    author: Mapped["Author"] = relationship(back_populates="books")

book.author.name    # 通过对象直接拿作者名,不用手写 JOIN
author.books        # 自动查出这个作者的所有书

7. 实战项目解析:04_sqlalchemy(部门/员工)系

7.1 文件地图

文件 作用 建模风格
base.py 生成模型基类 Base 老版 1.x (declarative_base)
database.py 引擎 + 会话工厂(全项目共用) 核心配置
models.py 手写模型:部门/员工 老版 1.x (Column)
main.py CRUD + 关联查询演示 老版 query API
gen.py 逆向工具 + 插测试数据 脚本
table_2_models.py 从数据库表自动生成的模型 新版 2.x (Mapped)
fastapi_sqlalchemy.py FastAPI 集成示例 用 table_2_models 的模型

核心设计:

复制代码
departments 部门表(一方)     id, name(唯一), location
employees   员工表(多方)     id, name, age, hire_date, department_id(外键)

三种玩法:

  1. 手写模型 → 建表 → CRUD(models.py + main.py
  2. 数据库已有表 → 自动生成模型(gen.py + table_2_models.py)
  3. 模型 + FastAPI → Web 接口(fastapi_sqlalchemy.py)

7.2 base.py ------ 所有模型的"祖先"

复制代码
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()   # 生成基类,所有模型需继承该类

每张表 = 一个继承 Base 的类。Base.metadata 统一登记所有表的元数据,create_all 遍历登记册建表。

7.3 database.py ------ 连接配置中心

复制代码
DATABASE_URL = "mysql+pymysql://root:***@localhost:3306/fastapi_db"
# 格式:mysql+pymysql:// 用户名:密码@主机:端口/数据库名

engine = create_engine(
    DATABASE_URL,
    echo=True,          # 控制台打印 SQL(调试神器)
    pool_pre_ping=True  # 用连接前先 ping,防连接失效
)

SessionLocal = sessionmaker(
    autocommit=False,   # 不自动提交,需手动 commit()
    bind=engine
)
  • sessionmaker = 会话工厂,生产 Session 的机器
  • 注释写"相当于 cursor 对象"不准确:Session 是工作单元 + 事务边界

7.4 models.py ------ 手写模型(一对多经典写法)

复制代码
class Department(Base):
    __tablename__ = "departments"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(50), nullable=False, unique=True)  # 部门名称唯一
    location = Column(String(100))

    # 一对多:一个部门 → 多个员工(返回列表)
    employees = relationship(
        "Employee",
        back_populates="department",
        lazy="selectin"      # 查部门时顺带用 IN 查询取员工
    )

class Employee(Base):
    __tablename__ = "employees"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(50), nullable=False)
    age = Column(Integer)
    hire_date = Column(Date)

    # 外键:绑定到 departments.id;RESTRICT = 部门有员工就不许删
    department_id = Column(
        Integer,
        ForeignKey("departments.id", ondelete="RESTRICT"),
        nullable=False
    )

    # 多对一:返回单个部门对象
    department = relationship(
        "Department",
        back_populates="employees",
        lazy="joined"        # 查员工时 JOIN 出部门,少一次查询
    )

要点:

  • 外键列 department_id + 关联属性 department 是两回事:外键存 id,relationship 存对象
  • 双向关联用 back_populates 互相指名
  • lazy 策略:selectin(额外 IN 查询)/ joined(JOIN 出来)/ 默认(用时才查,懒加载)

7.5 main.py ------ CRUD 综合演示(老版 query API)

函数 操作 关键代码
create_table() 建表 Base.metadata.create_all(bind=engine)
insert_data() 建部门→commit→refresh 拿 id→add_all([emp1,emp2])→commit
delete_data() query.filter(name=="李四").first()delete() → commit
update_data() 查到对象 → 改属性 → commit(无需 update 语句)
find_data() 见下方查询姿势大全

查询姿势大全(find_data):

复制代码
# 按主键查
dept = session.get(Department, 1)

# 条件过滤
rd = session.query(Employee).filter(Employee.department_id == 1).all()

# 逻辑运算 and_ / or_
from sqlalchemy import and_, or_
emp = session.query(Employee).filter(
    and_(Employee.age.between(30, 40), Employee.department_id == 1)
).first()

# 表连接 join(内连接:员工 + 所属部门名)
result = session.query(Employee, Department).join(
    Department, Employee.department_id == Department.id
).all()
for emp, dept in result:
    print(f"员工 {emp.name} 属于 {dept.name}")

# 预加载 joinedload(避免 N+1)
from sqlalchemy.orm import joinedload
employees = session.query(Employee).options(
    joinedload(Employee.department)
).all()

# 子查询:统计每个部门员工数,筛员工数>0 的部门
from sqlalchemy import func
dept_emp_count = session.query(
    Employee.department_id,
    func.count(Employee.id).label("count")
).group_by(Employee.department_id).subquery()

depts = session.query(Department).join(
    dept_emp_count, Department.id == dept_emp_count.c.department_id
).filter(dept_emp_count.c.count > 0).all()

# 去重 distinct
locations = session.query(Department.location).join(Employee).distinct().all()

# first() 取一条 / all() 取全部
first_emp = session.query(Employee).first()
all_depts = session.query(Department).all()

标准会话套路(每个函数都这样):

复制代码
session = SessionLocal()
try:
    ...操作 + session.commit()
except Exception as e:
    session.rollback()     # 出错回滚
    print(f"失败:{e}")
finally:
    session.close()        # 一定关闭

7.6 gen.py ------ 逆向工程 + 插数据

复制代码
# 1) 表 → 模型:调用 sqlacodegen 自动扫描数据库生成模型文件
cmd = [venv_python, "-m", "sqlacodegen", url]   # url 指向 fastapi_db
# 结果写入 table_2_models.py

# 2) 插测试数据:with 上下文管理器自动关会话(等价 try/finally)
with Session(engine) as session:
    session.add(dept)
    session.commit()

适合场景:表是 DBA/别人建好的,不想手写模型,直接生成,保证和真实结构 100% 一致(索引、外键约束、默认值都会搬过来)。

7.7 table_2_models.py ------ 自动生成的模型(2.x 风格)

复制代码
class Base(DeclarativeBase):      # ← 注意:和 base.py 的 Base 不是同一个!
    pass

class Departments(Base):
    __tablename__ = 'departments'
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(50), nullable=False)
    location: Mapped[Optional[str]] = mapped_column(String(100))
    created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
        DateTime, server_default=text('CURRENT_TIMESTAMP'))
    employees: Mapped[list['Employees']] = relationship('Employees', back_populates='department')

class Employees(Base):
    __tablename__ = 'employees'
    __table_args__ = (
        ForeignKeyConstraint(['department_id'], ['departments.id'],
                             ondelete='SET NULL', name='employees_ibfk_1'),
        Index('idx_dept', 'department_id')
    )
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    ...
    department: Mapped[Optional['Departments']] = relationship('Departments', back_populates='employees')

数据库里的外键约束(ON DELETE SET NULL)、索引、默认值都被原样还原了。

7.8 fastapi_sqlalchemy.py ------ FastAPI 集成

复制代码
# 依赖项:获取数据库会话
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 新增部门(INSERT)
@app.post("/departments/")
def create_department(name: str, location: str, db: Session = Depends(get_db)):
    db_department = Departments(name=name, location=location)
    db.add(db_department)
    db.commit()
    db.refresh(db_department)
    return db_department

# 查全部
@app.get("/departments/")
def read_departments(db: Session = Depends(get_db)):
    return db.query(Departments).all()

# 查单个,找不到抛 404
@app.get("/departments/{department_id}")
def read_department(department_id: int, db: Session = Depends(get_db)):
    department = db.query(Departments).filter(Departments.id == department_id).first()
    if not department:
        raise HTTPException(status_code=404, detail="Department not found")
    return department

# 代码中直接启动:uvicorn.run(app="fastapi_sqlalchemy:app", host="0.0.0.0", port=8000, reload=True)

7.9 坑与注意(务必记住)

一句话串起项目 :手写模型告诉 SQLAlchemy 表长什么样 → database.py 提供引擎和会话 → main.py 用 CRUD 灌数据并演示各种查询 → gen.py 演示从库里的表逆向生成模型 → fastapi_sqlalchemy.py 用这些模型对外提供 HTTP 接口。


8. 部署:uvicorn 与 nginx

8.1 两者关系:前台 + 后厨(不是二选一)

nginx uvicorn
Web服务器/反向代理 Python 应用服务器(ASGI)
C 语言写的,超快 跑你的 FastAPI 代码
管"流量进出" 管"业务逻辑"
不管 Python 不懂 HTTP 细节之外的业务
端口通常 80/443 端口通常 8000
一个顶十万连接 单进程能力有限

类比餐厅:nginx = 前台接待(迎客、排号分流、检查证件 HTTPS、拦闹事);uvicorn = 后厨团队,真正炒菜(跑 FastAPI)。

8.2 生产环境典型架构

复制代码
用户浏览器
   │
   ▼
nginx (80/443 端口)         ← 唯一对外的门
   │  反向代理,转发
   ├──────────┬──────────┐
   ▼          ▼          ▼
uvicorn①   uvicorn②   uvicorn③   ← 多个实例(多核并行)
   │          │          │
   ▼          ▼          ▼
FastAPI    FastAPI    FastAPI
   │          │          │
   └──────────┴──────────┘
              ▼
        MySQL / Redis

8.3 为什么需要 nginx(uvicorn 的短板)

短板 nginx 的解法
单进程扛不住大流量 同时挂几万个连接
多核利用率低 多个 uvicorn + 负载均衡,挂一个自动跳过
HTTPS 证书配置麻烦 SSL 终结在 nginx,内部走明文 http
静态文件也走 Python 慢 图片/CSS/JS 由 nginx 直接吐,快几十倍
裸奔在公网 隐藏真实端口、限流、拦恶意 IP
部署重启有断连 逐个重启无感知

nginx 核心配置:

复制代码
upstream fastapi_backend {          # 后厨团队
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
}

server {
    listen 80;
    location / {
        proxy_pass http://fastapi_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;   # 让后端知道真实IP
    }
    location /static/ {              # 静态文件 nginx 自己处理
        alias /app/static/;
    }
}

8.4 路由分发的两层分工(重点辨析)

nginx 做"大门级"分发(到服务为止);FastAPI 做"房间级"分发(到函数为止)

分发层级 nginx FastAPI
看什么 域名 + URL 前缀(location) 完整路径 + HTTP方法 + 参数
粒度 服务级 函数级
典型规则 location /api/ → uvicorn @app.get("/books/{book_id}")
解析参数 不关心 /books/3 的 3 把 3 提取成 book_id
类比 大楼前台总机:转哪个部门 部门内部分机找谁

nginx 多服务分发示例:

复制代码
server {
    location /api/    { proxy_pass http://fastapi_backend; }   # 动态接口
    location /admin/  { proxy_pass http://admin_backend; }     # 后台
    location /static/ { alias /var/www/static/; }              # 静态
    location /        { proxy_pass http://frontend; }          # 前端页面
}

一句口诀:nginx 分到"服务"为止,FastAPI 分到"函数"为止。nginx 不问 /books/3 里的 3 是什么;FastAPI 才把 3 变成 book_id。

判断该在哪层分发:

  • nginx:不同技术栈服务并存、静态/动态分离、按域名分网站
  • FastAPI:同一应用内部所有业务接口

❌ 反面教材:nginx 里写 location /books/3 具体路径------路径一变就得改配置。 ✅ 正确:nginx 永远只写前缀级宽规则,具体资源一律交给 FastAPI 动态匹配。

相关推荐
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 14 章 打包与部署 制作 Docker 镜像 阅读笔记 43
spring boot·笔记·docker
噜~噜~噜~1 小时前
操作系统笔记-2.4.1 死锁的概念
笔记·操作系统
Aime_Perfect1 小时前
sqlserver always on
服务器·数据库·sqlserver
摘星编程1 小时前
从“数据出库“到“模型入库“:DolphinDB 库内机器学习全流程实践
数据库
血小板要健康1 小时前
队列 + 宽搜(BFS):二叉树层序遍历 算法总结
java·数据结构·笔记·算法·leetcode·宽度优先
Sagittarius_A*1 小时前
【好靶场】SQL注入-时间盲注
数据库·sql
醉舞经阁半卷书11 小时前
量化交易基础之python库学习
学习
youm20032 小时前
【学习笔记】认识NoSQL——非关系型数据库
笔记·学习·nosql
神明不懂浪漫2 小时前
【第二章】库、表、增删改查操作
开发语言·数据库·经验分享·笔记