FastAPI 数据库操作:创建与管理多个模型的应用

FastAPI 数据库操作:创建与管理多个模型的应用


推荐阅读 📚🎓🧑‍🏫

[1] 一起学Python 专栏:深入探讨 Python 编程,涵盖基础与进阶内容,以及 NumPyPandasMatplotlibDockerLinux 等实用技术。


本文介绍了如何在 FastAPI 应用中使用 SQLModel 进行数据库操作,支持多模型管理。通过示例代码展示了如何创建、读取、更新、删除(CRUD)多个模型,包括使用 SQLModel 的继承机制避免重复定义字段。文章详细介绍了模型的定义、API 的构建、以及如何利用 FastAPI 自动处理数据库会话与请求验证。同时,强调了如何为不同的业务需求定义多种 Pydantic 数据模型,并通过 response_model 实现数据返回和格式化的自动化。

预备课FastAPI 数据库操作:创建与管理单一模型的应用程序

以下示例中使用的 Python 版本为 Python 3.10.15,FastAPI 版本为 0.115.4

一 示例代码

from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
import logging

# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("sqlalchemy.engine")
logger.setLevel(logging.INFO)


class HeroBase(SQLModel):
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)


class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str


class HeroPublic(HeroBase):
    id: int


class HeroCreate(HeroBase):
    secret_name: str


class HeroUpdate(HeroBase):
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def get_session():
    with Session(engine) as session:
        yield session


app = FastAPI()


@app.on_event("startup")
def on_startup():
    create_db_and_tables()


@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: Session = Depends(get_session)):
    db_hero = Hero.model_validate(hero)
    session.add(db_hero)
    session.commit()
    session.refresh(db_hero)
    return db_hero


@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(
        session: Session = Depends(get_session),
        offset: int = 0,
        limit: int = Query(default=100, le=100),
):
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
    return heroes


@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: Session = Depends(get_session)):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    return hero


@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(
        hero_id: int, hero: HeroUpdate, session: Session = Depends(get_session)
):
    hero_db = session.get(Hero, hero_id)
    if not hero_db:
        raise HTTPException(status_code=404, detail="Hero not found")
    hero_data = hero.model_dump(exclude_unset=True)
    hero_db.sqlmodel_update(hero_data)
    session.add(hero_db)
    session.commit()
    session.refresh(hero_db)
    return hero_db


@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: Session = Depends(get_session)):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    session.delete(hero)
    session.commit()
    return {"ok": True}

运行代码文件 db02.py 来启动应用:

$ uvicorn db02:app --reload

SwaggerUI 中可以查看在线文档:http://127.0.0.1:8000/docs

二 创建多个 Pydantic 模型

在 SQLModel 中,带有 table=True 的模型类是表模型,没带 table=True 的则是数据模型,本质上它们都是 Pydantic 模型。通过 SQLModel 的继承机制,可以避免重复定义字段。

# HeroBase 是所有英雄模型的基类,包含基本的字段定义
class HeroBase(SQLModel):
    name: str = Field(index=True)  # 英雄的名字,索引字段
    age: int | None = Field(default=None, index=True)  # 英雄的年龄,索引字段,默认值为 None

# Hero 是包含表结构的完整模型,继承自 HeroBase
class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)  # 主键 id,默认值为 None
    secret_name: str  # 英雄的秘密名字

# HeroPublic 公开的英雄数据模型,继承自 HeroBase
class HeroPublic(HeroBase):
    id: int  # 只包含 id 字段

# HeroCreate 用于创建新的英雄实例,继承自 HeroBase
class HeroCreate(HeroBase):
    secret_name: str  # 创建时需要提供英雄的秘密名字

# HeroUpdate 用于更新英雄数据,继承自 HeroBase
class HeroUpdate(HeroBase):
    name: str | None = None  # 可选的更新字段:英雄名字
    age: int | None = None  # 可选的更新字段:英雄年龄
    secret_name: str | None = None  # 可选的更新字段:英雄的秘密名字
  • HeroBase 是一个基础类,包含了所有英雄共有的字段。它不包含数据库表的属性,因此没有 table=True
  • Hero 是包含表结构的模型类,继承了 HeroBase,并且通过 table=True 被定义为数据库表模型。它还包含了唯一的 id 字段和 secret_name 字段。
  • HeroPublic 用于公开展示英雄数据,不包含敏感信息(如 secret_name)。
  • HeroCreate 用于新建英雄时的数据结构,要求提供 secret_name
  • HeroUpdate 用于更新已有英雄的数据,所有字段都是可选的。

三 用 HeroCreate 创建并返回 HeroPublic

# 创建英雄的 POST 请求接口
@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: Session = Depends(get_session)):
    """
    处理创建新的英雄的请求。
    - 接收 HeroCreate 数据模型作为请求体。
    - 将数据保存到数据库并返回创建的英雄数据(仅公开信息)。
    """

    # 使用 Hero.model_validate() 方法将传入的 HeroCreate 数据验证并转换为 Hero 实例
    db_hero = Hero.model_validate(hero)

    # 将新英雄对象添加到数据库会话中
    session.add(db_hero)

    # 提交事务,将新英雄数据保存到数据库
    session.commit()

    # 刷新 db_hero,以确保它包含数据库中的最新状态(包括生成的 id)
    session.refresh(db_hero)

    # 返回保存后的英雄数据(公开信息)
    return db_hero

使用 response_model=HeroPublic 来代替返回类型注释 -> HeroPublic,因为返回的实际值是 Hero 而非 HeroPublic。如果使用 -> HeroPublic,编辑器和代码检查工具会报错。通过 response_model 声明,FastAPI 会自行处理返回值,而不干扰类型注解或工具提示。

四 用 HeroPublic 读取 Hero

# 获取所有英雄的 GET 请求接口
@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(
        session: Session = Depends(get_session),  # 注入数据库会话
        offset: int = 0,  # 可选参数:分页偏移量,默认从第0条数据开始
        limit: int = Query(default=100, le=100),  # 可选参数:分页数量,默认最多返回100条数据,且最大为100
):
    """
    处理获取英雄列表的请求。
    - 支持分页,返回分页后的英雄数据列表。
    - 只返回公开的英雄信息(HeroPublic)。
    """

    # 执行 SQL 查询,选择 Hero 表中的所有英雄数据,应用分页(offset 和 limit)
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()

    # 返回查询结果,符合 HeroPublic 的数据模型
    return heroes

五 用 HeroPublic 读取单个 Hero

# 获取单个英雄的 GET 请求接口
@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: Session = Depends(get_session)):
    """
    处理获取单个英雄的请求。
    - 根据提供的 hero_id 查询英雄数据。
    - 如果英雄不存在,返回 404 错误。
    """

    # 根据 hero_id 从数据库中查询对应的英雄
    hero = session.get(Hero, hero_id)

    # 如果没有找到对应的英雄,抛出 404 错误
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")

    # 返回找到的英雄数据,符合 HeroPublic 模型
    return hero

六 用 HeroUpdate 更新单个 Hero

# 更新指定英雄信息的 PATCH 请求接口
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(
        hero_id: int,  # 传入的英雄 ID,用于确定更新目标
        hero: HeroUpdate,  # 传入的英雄更新数据
        session: Session = Depends(get_session)  # 通过依赖注入获取数据库会话
):
    """
    处理更新指定英雄信息的请求。
    - 根据提供的 hero_id 查询英雄数据并更新。
    - 如果英雄不存在,返回 404 错误。
    """

    # 根据 hero_id 从数据库中获取当前英雄对象
    hero_db = session.get(Hero, hero_id)

    # 如果没有找到对应的英雄,抛出 404 错误
    if not hero_db:
        raise HTTPException(status_code=404, detail="Hero not found")

    # 使用 model_dump() 方法获取更新的数据,并排除未设置的字段(只包含已提供的数据)
    # exclude_unset=True 用于排除模型中未显式设置的字段,即忽略声明时的默认值。
    # 这样仅会包含实际提供的字段,避免更新不必要的默认值。
    hero_data = hero.model_dump(exclude_unset=True)

    # 使用 sqlmodel_update() 方法将新的数据更新到现有的 hero_db 实例
    hero_db.sqlmodel_update(hero_data)

    # 将更新后的英雄对象添加到会话中
    session.add(hero_db)

    # 提交事务,将更新保存到数据库
    session.commit()

    # 刷新 hero_db 对象,确保它包含数据库中的最新数据
    session.refresh(hero_db)

    # 返回更新后的英雄数据,符合 HeroPublic 模型
    return hero_db

实际上 session.add(db_hero) 是标记对象(无论是新增还是更新)准备提交。对于更新,SQLAlchemy 会自动检测对象的主键(如 id)是否与数据库中的记录匹配,并决定是插入新记录还是更新现有记录。更新过程 如下:

  • 查找对象:首先,通过 session.get() 或其他查询方式获取一个已存在的对象(例如 db_hero)。
  • 修改对象:修改 db_hero 的字段值(例如修改 db_hero.name)。
  • 标记修改:调用 session.add(db_hero),将其标记为待提交的修改。
  • 提交事务:session.commit() 会检查所有标记为“已修改”的对象,并将其更新到数据库中。

七 删除单个 Hero

# 删除指定英雄的 DELETE 请求接口
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: Session = Depends(get_session)):
    """
    处理删除指定英雄的请求。
    - 根据提供的 hero_id 删除对应的英雄数据。
    - 如果英雄不存在,返回 404 错误。
    """

    # 根据 hero_id 从数据库中获取对应的英雄对象
    hero = session.get(Hero, hero_id)

    # 如果没有找到对应的英雄,抛出 404 错误
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")

    # 删除从数据库中获取的英雄对象
    session.delete(hero)

    # 提交事务,将删除操作应用到数据库
    session.commit()

    # 返回一个表示删除成功的响应
    return {"ok": True}

八 完整代码示例

from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
import logging

# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("sqlalchemy.engine")
logger.setLevel(logging.INFO)


class HeroBase(SQLModel):
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)


class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str


class HeroPublic(HeroBase):
    id: int


class HeroCreate(HeroBase):
    secret_name: str


class HeroUpdate(HeroBase):
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def get_session():
    with Session(engine) as session:
        yield session


app = FastAPI()


@app.on_event("startup")
def on_startup():
    create_db_and_tables()


@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: Session = Depends(get_session)):
    db_hero = Hero.model_validate(hero)
    session.add(db_hero)
    session.commit()
    session.refresh(db_hero)
    return db_hero


@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(
        session: Session = Depends(get_session),
        offset: int = 0,
        limit: int = Query(default=100, le=100), ):
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
    return heroes


@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: Session = Depends(get_session)):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    return hero


@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(
        hero_id: int, hero: HeroUpdate, session: Session = Depends(get_session)):
    hero_db = session.get(Hero, hero_id)
    if not hero_db:
        raise HTTPException(status_code=404, detail="Hero not found")
    hero_data = hero.model_dump(exclude_unset=True)
    hero_db.sqlmodel_update(hero_data)
    session.add(hero_db)
    session.commit()
    session.refresh(hero_db)
    return hero_db


@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: Session = Depends(get_session)):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    session.delete(hero)
    session.commit()
    return {"ok": True}

九 源码地址

详情见:GitHub FastApiProj

十 参考

[1] FastAPI 文档

[2] SQLModel 文档

[3] SQLAlchemy 官网

[4] SQLModel 使用教程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲代码不忘补水

感谢有你,让我的创作更有价值!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值