FastAPI 数据库操作:创建与管理多个模型的应用
推荐阅读 📚🎓🧑🏫
[1] 一起学Python 专栏:深入探讨 Python 编程,涵盖基础与进阶内容,以及 NumPy、Pandas、Matplotlib、Docker、Linux 等实用技术。
本文介绍了如何在 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}
九 源码地址
十 参考
[1] FastAPI 文档
[2] SQLModel 文档
[3] SQLAlchemy 官网
[4] SQLModel 使用教程