FastAPI + MySQL 实战:从零搭建带数据库的 CRUD 接口

举报
yd_228838069 发表于 2026/10/30 10:56:55 2026/10/30
【摘要】 在上一篇 FastAPI 入门教程中,我们已经学会了如何创建 Web API、定义路由和处理请求参数。不过,如果数据只保存在 Python 的内存字典中,应用重启后数据就会丢失,无法满足真实项目的需求。因此,本文将进一步介绍如何使用 FastAPI 连接 MySQL 数据库,通过 SQLAlchemy 2.0 实现完整的 CRUD 操作,并使用 Pydantic 进行数据校验。完成本教程后,...

在上一篇 FastAPI 入门教程中,我们已经学会了如何创建 Web API、定义路由和处理请求参数。不过,如果数据只保存在 Python 的内存字典中,应用重启后数据就会丢失,无法满足真实项目的需求。

因此,本文将进一步介绍如何使用 FastAPI 连接 MySQL 数据库,通过 SQLAlchemy 2.0 实现完整的 CRUD 操作,并使用 Pydantic 进行数据校验。

完成本教程后,你将拥有一个可以创建、查询、修改和删除任务的 Web API,并且数据会持久保存在 MySQL 中。

一、项目技术栈

本项目采用以下技术:

技术 用途
Python 3.10+ 后端开发语言
FastAPI Web API 框架
MySQL 8.0+ 关系型数据库
SQLAlchemy 2.0 ORM 数据库操作
PyMySQL Python MySQL 驱动
Pydantic 请求参数与数据校验
Uvicorn ASGI 应用服务器

SQLAlchemy ORM 可以让开发者通过 Python 类和对象操作数据库,减少直接编写 SQL 的工作量,同时保留必要的 SQL 查询能力。

本教程使用同步数据库连接,便于初学者理解。对于一般规模的入门项目,这是一种清晰、实用的起步方式。

二、创建项目并安装依赖

1. 创建项目目录

在终端中执行:

mkdir fastapi-mysql-demo
cd fastapi-mysql-demo

创建虚拟环境。

Windows:

python -m venv .venv
.venv\Scripts\activate

Debian 或其他 Linux 系统:

python3 -m venv .venv
source .venv/bin/activate

如果 Debian 缺少虚拟环境组件,可以执行:

sudo apt update
sudo apt install python3-venv

2. 安装项目依赖

python -m pip install "fastapi[standard]" sqlalchemy pymysql

如果你的环境使用 python3 命令,也可以相应地使用:

python3 -m pip install "fastapi[standard]" sqlalchemy pymysql

安装完成后,可以保存依赖清单:

python -m pip freeze > requirements.txt

这样,在其他环境部署时,就可以根据依赖文件重新安装项目所需的软件包。

三、配置 MySQL 数据库

1. 创建数据库

首先确保 MySQL 已经安装并运行。

使用具有创建数据库权限的 MySQL 账户登录:

mysql -u root -p

输入密码后,执行以下 SQL:

CREATE DATABASE fastapi_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

这里创建了名为 fastapi_demo 的数据库,并使用 utf8mb4 字符集,以便正确存储中文和其他 Unicode 字符。

2. 创建专用数据库账户

正式项目不建议直接使用 MySQL 的 root 账户连接应用。可以创建一个专门用于本项目的数据库用户:

CREATE USER 'fastapi_user'@'localhost'
IDENTIFIED BY '替换为高强度随机密码';

GRANT SELECT, INSERT, UPDATE, DELETE
ON fastapi_demo.*
TO 'fastapi_user'@'localhost';

FLUSH PRIVILEGES;

请将示例密码替换为真实的强密码。

上述授权适用于应用连接本机 MySQL 的常见场景。如果应用需要执行数据库迁移,还应根据迁移方式单独规划所需的 DDL 权限,而不是直接授予所有权限。

如果 FastAPI 和 MySQL 不在同一台机器上,数据库账户的主机匹配规则也需要根据实际网络环境调整。

3. 测试数据库连接

退出 MySQL 后,可以使用新账户测试:

mysql -u fastapi_user -p fastapi_demo

如果能够成功进入数据库,说明数据库及账户配置基本完成。

四、设计项目目录结构

为了避免所有代码都堆积在一个文件中,建议采用以下结构:

fastapi-mysql-demo/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── database.py
│   ├── models.py
│   └── schemas.py
├── .env
├── .gitignore
└── requirements.txt

各文件的作用如下:

  • main.py:定义 API 路由和业务处理逻辑。

  • database.py:管理数据库连接、会话和依赖注入。

  • models.py:定义 SQLAlchemy ORM 数据模型。

  • schemas.py:定义请求体和响应数据结构。

  • .env:保存本地环境配置。

  • .gitignore:防止提交虚拟环境和敏感配置。

这种结构适合小型示例项目。后续增加用户管理、文章管理等模块时,可以继续拆分 routers、services 和 repositories。

五、配置数据库连接

在项目根目录创建 .env 文件:

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=fastapi_demo
DB_USER=fastapi_user
DB_PASSWORD=替换为高强度随机密码

这里的配置值需要根据你的 MySQL 实际环境调整。

接下来修改 app/database.py:

import os
from urllib.parse import quote_plus

from sqlalchemy import URL, create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker


def get_database_url():
    password = os.environ["DB_PASSWORD"]

    return URL.create(
        drivername="mysql+pymysql",
        username=os.environ["DB_USER"],
        password=password,
        host=os.environ.get("DB_HOST", "127.0.0.1"),
        port=int(os.environ.get("DB_PORT", "3306")),
        database=os.environ["DB_NAME"],
        query={"charset": "utf8mb4"},
    )


class Base(DeclarativeBase):
    pass


engine = create_engine(
    get_database_url(),
    pool_pre_ping=True,
)

SessionLocal = sessionmaker(
    bind=engine,
    autoflush=False,
    expire_on_commit=False,
)


def get_db():
    db = SessionLocal()

    try:
        yield db
    finally:
        db.close()

注意:SQLAlchemy 的 URL.create() 会处理数据库连接 URL 中的特殊字符,因此不需要手动拼接密码字符串。上面导入的 quote_plus 并未使用,可以删除该导入,不影响程序运行。

本示例使用操作系统环境变量读取配置。.env 文件不会被 Python 自动读取,因此还需要在启动前加载环境变量。

在开发环境加载 .env

安装 python-dotenv:

python -m pip install python-dotenv

在 app/database.py 顶部增加:

from dotenv import load_dotenv

load_dotenv()

这样,应用启动时就会尝试从项目根目录的 .env 文件读取配置。

正式部署时,也可以直接由 systemd、容器平台或其他进程管理工具注入环境变量。请勿将包含真实数据库密码的 .env 文件提交到公开代码仓库。

六、创建 SQLAlchemy 数据模型

接下来定义任务表。

在 app/models.py 中编写:

from datetime import datetime

from sqlalchemy import Boolean, DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column

from app.database import Base


class Task(Base):
    __tablename__ = "tasks"

    id: Mapped[int] = mapped_column(
        primary_key=True,
        index=True,
    )

    title: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
    )

    description: Mapped[str | None] = mapped_column(
        Text,
        nullable=True,
    )

    completed: Mapped[bool] = mapped_column(
        Boolean,
        default=False,
        nullable=False,
    )

    created_at: Mapped[datetime] = mapped_column(
        DateTime,
        server_default=func.now(),
        nullable=False,
    )

这里定义了一个名为 Task 的 ORM 模型,对应数据库中的 tasks 表。

字段说明:

字段 类型 用途
id 整数 主键,标识任务
title 字符串 任务标题
description 文本 任务描述,可为空
completed 布尔值 任务是否完成
created_at 日期时间 记录创建时间

SQLAlchemy 2.0 使用 Mapped 和 mapped_column() 定义类型明确的 ORM 字段。

需要注意,default=False 是 SQLAlchemy 提供的 Python 端默认值,而 server_default 则是在数据库服务器端设置默认值。实际项目中应根据数据写入方式明确默认值由哪一层负责。

七、创建 Pydantic 请求与响应模型

数据库模型和 API 数据模型承担不同职责。

数据库模型描述表结构,而 Pydantic 模型负责校验客户端输入、定义 API 输出格式。

在 app/schemas.py 中编写:

from datetime import datetime

from pydantic import BaseModel, ConfigDict, Field


class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=100)
    description: str | None = None
    completed: bool = False


class TaskUpdate(BaseModel):
    title: str | None = Field(
        default=None,
        min_length=1,
        max_length=100,
    )
    description: str | None = None
    completed: bool | None = None


class TaskResponse(BaseModel):
    id: int
    title: str
    description: str | None
    completed: bool
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)

三个模型的区别:

  • TaskCreate:创建任务时使用,要求提供有效标题。

  • TaskUpdate:修改任务时使用,允许只更新部分字段。

  • TaskResponse:向客户端返回任务信息。

from_attributes=True 允许 Pydantic 从 SQLAlchemy ORM 对象的属性读取数据并构建响应模型。

本例允许 description 为 None。如果需要限制描述长度,可以在创建和更新模型中为它增加相应的 Field 约束。

八、实现完整 CRUD 接口

现在将数据库模型、连接会话和 Pydantic 模型组合起来。

在 app/main.py 中编写:

from contextlib import asynccontextmanager
from collections.abc import AsyncIterator

from fastapi import Depends, FastAPI, HTTPException, Response, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.database import Base, engine, get_db
from app.models import Task
from app.schemas import TaskCreate, TaskResponse, TaskUpdate


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    # 仅用于本教程的入门演示。
    # 正式项目建议使用 Alembic 管理数据库迁移。
    Base.metadata.create_all(bind=engine)
    yield


app = FastAPI(
    title="FastAPI + MySQL 任务管理 API",
    version="1.0.0",
    lifespan=lifespan,
)


@app.get("/")
def home():
    return {"message": "FastAPI + MySQL 运行正常"}


@app.post(
    "/tasks",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
def create_task(
    payload: TaskCreate,
    db: Session = Depends(get_db),
):
    task = Task(**payload.model_dump())

    db.add(task)
    db.commit()
    db.refresh(task)

    return task


@app.get("/tasks", response_model=list[TaskResponse])
def list_tasks(
    skip: int = 0,
    limit: int = 20,
    db: Session = Depends(get_db),
):
    if skip < 0:
        raise HTTPException(
            status_code=400,
            detail="skip 不能小于 0",
        )

    if not 1 <= limit <= 100:
        raise HTTPException(
            status_code=400,
            detail="limit 必须在 1 到 100 之间",
        )

    statement = (
        select(Task)
        .order_by(Task.id.desc())
        .offset(skip)
        .limit(limit)
    )

    return db.scalars(statement).all()


@app.get("/tasks/{task_id}", response_model=TaskResponse)
def get_task(
    task_id: int,
    db: Session = Depends(get_db),
):
    task = db.get(Task, task_id)

    if task is None:
        raise HTTPException(
            status_code=404,
            detail="任务不存在",
        )

    return task


@app.patch("/tasks/{task_id}", response_model=TaskResponse)
def update_task(
    task_id: int,
    payload: TaskUpdate,
    db: Session = Depends(get_db),
):
    task = db.get(Task, task_id)

    if task is None:
        raise HTTPException(
            status_code=404,
            detail="任务不存在",
        )

    changes = payload.model_dump(exclude_unset=True)

    for field, value in changes.items():
        setattr(task, field, value)

    db.commit()
    db.refresh(task)

    return task


@app.delete(
    "/tasks/{task_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
def delete_task(
    task_id: int,
    db: Session = Depends(get_db),
):
    task = db.get(Task, task_id)

    if task is None:
        raise HTTPException(
            status_code=404,
            detail="任务不存在",
        )

    db.delete(task)
    db.commit()

    return Response(status_code=status.HTTP_204_NO_CONTENT)

代码中的关键点

1. 使用 Depends(get_db) 获取数据库会话

FastAPI 会调用 get_db(),在请求处理过程中提供数据库会话,并在依赖清理时关闭会话。

2. 创建记录后执行 commit()

db.add(task) 将对象加入当前会话,db.commit() 提交事务,db.refresh(task) 则重新读取数据库生成或更新的字段。

3. 使用 db.get() 查询单条记录

db.get(Task, task_id) 根据主键查找任务。如果没有对应记录,就返回 None。

4. 使用 exclude_unset=True 实现部分更新

例如,客户端只提交:

{
  "completed": true
}

程序只会修改 completed 字段,不会因为其他字段未提交而将其覆盖为默认值。

需要注意,显式提交 "description": null 与完全不提交 description 是不同的操作:前者会把描述设置为空值,后者则不会修改描述。

5. 删除操作返回 HTTP 204

删除成功后,接口返回 204 No Content,表示操作成功且没有响应正文。

九、启动项目并测试接口

在项目根目录执行:

fastapi dev app/main.py

如果数据库配置正确、MySQL 正常运行,应用启动时会尝试创建示例所需的 tasks 表。

然后访问:

http://127.0.0.1:8000/docs

FastAPI 会自动生成 Swagger UI 文档,可以在浏览器中直接测试全部接口。

1. 创建任务

请求:

POST /tasks
Content-Type: application/json

请求体:

{
  "title": "学习 FastAPI",
  "description": "完成数据库 CRUD 教程",
  "completed": false
}

预期返回类似:

{
  "id": 1,
  "title": "学习 FastAPI",
  "description": "完成数据库 CRUD 教程",
  "completed": false,
  "created_at": "2026-10-30T10:00:00"
}

实际 ID 和时间由数据库决定。

2. 查询任务列表

请求:

GET /tasks?skip=0&limit=20

返回任务数组,支持通过 skip 和 limit 控制分页。

3. 查询单个任务

请求:

GET /tasks/1

如果 ID 为 1 的任务存在,就返回该任务;如果不存在,则返回 HTTP 404。

4. 修改任务

请求:

PATCH /tasks/1
Content-Type: application/json

请求体:

{
  "completed": true
}

返回更新后的任务对象。

5. 删除任务

请求:

DELETE /tasks/1

如果任务存在,接口返回 HTTP 204;如果任务不存在,则返回 HTTP 404。

十、常见错误及解决方法

1. Access denied for user

常见原因包括数据库用户名或密码错误、账户主机匹配规则不正确,以及账户缺少所需权限。

建议使用项目专用数据库账户重新测试连接,并检查 MySQL 的账户授权。

2. Can't connect to MySQL server

可能是 MySQL 服务没有启动、端口配置错误,或者数据库只监听了特定网络地址。

Debian 可以检查服务状态:

sudo systemctl status mysql

还可以检查本机的 3306 端口监听情况:

sudo ss -lntp | grep 3306

如果 FastAPI 与 MySQL 位于同一台服务器,通常没有必要将 MySQL 的 3306 端口开放到公网。

3. ModuleNotFoundError

例如:

ModuleNotFoundError: No module named 'sqlalchemy'

这通常表示当前 Python 环境没有安装相应依赖,或者启动程序时没有使用正确的虚拟环境。

可以先激活虚拟环境,再安装:

python -m pip install -r requirements.txt

4. 数据库表不存在

请检查 models.py 中是否定义了模型,以及 main.py 是否导入了对应模型。

本教程使用 Base.metadata.create_all() 创建缺失的表,但它不会自动将已有表结构升级到最新版本。

正式项目应使用 Alembic 管理数据库迁移,避免直接依赖启动时自动建表来维护生产数据库结构。

十一、正式项目还需要完善哪些内容?

本教程实现了基础 CRUD,但距离生产级应用还有一些工作需要完成。

数据库迁移

使用 Alembic 管理表结构变更,记录每次数据库升级,避免手动修改数据库导致代码与表结构不一致。

身份认证与权限管理

根据业务需求引入登录认证、角色权限和资源访问控制。不能因为用户能够访问 API,就允许其随意修改或删除其他用户的数据。

统一异常处理

对数据库异常、业务异常和参数错误进行合理处理,避免向客户端暴露数据库连接信息或内部堆栈。

日志和监控

记录请求、异常和关键业务操作,便于排查线上问题。

分页与查询优化

根据实际数据规模增加过滤、排序、索引和分页策略。数据量较大时,还应评估深分页的性能问题。

配置与部署安全

通过环境变量或安全配置系统管理密钥;生产环境采用适当的进程管理、HTTPS、反向代理和访问控制。

十二、总结

通过本教程,我们完成了一个基于 FastAPI、MySQL 和 SQLAlchemy 2.0 的任务管理 API。

整个项目包含以下核心环节:

  1. 创建 Python 虚拟环境并安装依赖。

  2. 创建 MySQL 数据库和专用账户。

  3. 使用 SQLAlchemy 定义 ORM 模型。

  4. 使用 Pydantic 校验请求和响应数据。

  5. 通过 FastAPI 实现增删改查。

  6. 使用 Swagger UI 测试接口。

  7. 了解数据库迁移、部署和安全方面的后续工作。

如果你已经掌握 Python 基础,这个项目可以作为后端开发的第一个数据库实战练习。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。