FastAPI异步ORM增删改实战:从单表操作到一对多关联查询

文章目录

  • 前言
  • 一、单表的增加操作
  • 二、单表的删除操作
  • 三、单表的更新操作
  • 四、关联关系
    • [4.1 一对一(1:1)](#4.1 一对一(1:1))
    • [4.2 1:M](#4.2 1:M)
      • [4.2.1 怎么在模型里表达这种关系](#4.2.1 怎么在模型里表达这种关系)
      • [4.2.2 添加部门的同时添加用户](#4.2.2 添加部门的同时添加用户)
      • [4.2.3 根据部门名称查询该部门的员工](#4.2.3 根据部门名称查询该部门的员工)
      • [4.2.4 根据用户名称查询所在部门](#4.2.4 根据用户名称查询所在部门)
    • [4.3 多对多(M:N)](#4.3 多对多(M:N))
  • 结语

前言

会查数据库只是第一步,真正的后端开发离不开增删改与表之间的关联。本文从单表操作到一对多关系,带你走完FastAPI异步ORM的完整实战闭环。

一、单表的增加操作

增加数据就是往数据库里插入一条新记录。在 ORM 里,你有两种办法可以做到这件事。

第一种办法是直接写插入语句,就像写 SQL 一样。这种办法比较生硬,一般用得不多。

第二种办法是先创建一个 Python 对象,然后把这个对象交给 Session,让 Session 帮你存进数据库。这种办法更符合 ORM 的思想,代码也更好读。

在写接口之前,我们先要定义一个数据模型。这个模型用来告诉 FastAPI:调用接口的人需要传哪些字段。

python 复制代码
from pydantic import BaseModel
from datetime import datetime

class UserRequest(BaseModel):
    name: str
    password: str
    salary: float
    birthday: datetime

这个 UserRequest 是一个 Pydantic 模型。它的作用就是检查用户传过来的数据格式对不对。比如 salary 必须是数字,birthday 必须是日期时间格式。

接下来我们写添加用户的接口:

python 复制代码
from fastapi import FastAPI, Depends, HTTPException
from starlette import status
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()

@app.post("/users")
async def add_user(user: UserRequest, session: AsyncSession = Depends(get_session)):
    # 把 Pydantic 对象里的数据拿出来
    name = user.name
    password = user.password
    salary = user.salary
    birthday = user.birthday

    # 用这些数据创建一个 User 对象
    user_object = User(name=name, password=password, salary=salary, birthday=birthday)

    # 把这个对象加到 Session 里
    session.add(user_object)

    return {
        "code": 200,
        "message": "用户添加成功"
    }

代码解释:

  • user: UserRequest 表示这个接口接收一个 JSON 请求体,FastAPI 会自动把它转成 UserRequest 对象
  • user.nameuser.password 这些就是从请求体里取出来的数据
  • User(...) 是创建了一个数据库模型对象,这个对象对应数据库表里的一行数据
  • session.add(user_object) 把这行数据放进了 Session 的"待办清单"里。等 Session 提交的时候,它才会真正写进数据库
  • 因为 get_session 依赖在请求结束时会自动调用 commit(),所以我们这里不用手动写 await session.commit()

注意事项:

  • 添加之前你可以判断一下数据是不是空的。如果 user 是空值,就返回 400 错误
  • 在真正的项目里,密码不能直接明文存进数据库,你要先加密。这里为了演示方便,就先写成明文

添加用户运行结果:

二、单表的删除操作

删除数据不是直接执行删除那么简单。你要先确认这条数据真的存在,不然删了一个不存在的东西,程序就会出错。

另外,在真正的项目里,删除通常分成两种:

  • 物理删除:直接从数据库里把这条记录删掉,再也找不回来
  • 逻辑删除 :不真的删掉,只是把这条记录标记为"已删除"(比如加一个 is_deleted 字段设为 True)。这样以后还能查出来,数据也不会丢

下面这个例子用的是物理删除:

python 复制代码
from sqlalchemy import Select, Delete

@app.delete("/users/{id}")
async def delete_user(id: int, session: AsyncSession = Depends(get_session)):
    # 第一步:先查出这个用户
    stmt = Select(User).where(User.id == id)
    result = await session.execute(stmt)

    # scalar_one_or_none() 的意思是:查得到就返回一个对象,查不到就返回 None
    user = result.scalar_one_or_none()

    # 如果用户不存在,就返回 404 错误
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )

    # 第二步:执行删除语句
    stmt = Delete(User).where(User.id == id)
    await session.execute(stmt)

    return {
        "code": 200,
        "message": "用户删除成功"
    }

代码解释:

  • 第一步先用 Select 查询这个用户存不存在。这是为了防止删错
  • scalar_one_or_none() 很适合这种"查一个或者没有"的场景。如果你用 scalar(),查不到的时候可能会出错
  • 如果用户存在,再用 Delete 语句删掉它
  • 这里同样不用手动写 commit(),因为依赖项会帮我们提交

删除用户运行结果:

关于关联数据的提醒:

如果这个用户还关联了其他数据(比如他发表了很多文章),你要先想清楚怎么办。常见做法有:

  • 一起删掉关联数据(级联删除)
  • 把关联数据的外键设为 NULL
  • 不允许删除,先让用户手动处理关联数据

具体用哪种办法,要看你的业务需求。

三、单表的更新操作

更新数据的意思是:查出原来的记录,然后把它的某些字段改成新值。

更新和添加有点像,你也要先定义一个 Pydantic 模型,告诉接口需要传哪些新数据:

python 复制代码
class UserUpdate(BaseModel):
    id: int
    name: str
    password: str
    salary: float
    birthday: datetime

然后写更新接口:

python 复制代码
@app.put("/users/{id}")
async def update_user(
    id: int,
    user_request: UserUpdate,
    session: AsyncSession = Depends(get_session)
):
    # 第一步:根据 id 查出原来的用户
    stmt = Select(User).where(User.id == id)
    result = await session.execute(stmt)
    user = result.scalar_one_or_none()

    # 如果用户不存在,这里应该返回错误。为了代码简洁,示例里省略了判断

    # 第二步:用新数据覆盖旧数据
    user.name = user_request.name
    user.password = user_request.password
    user.salary = user_request.salary
    user.birthday = user_request.birthday

    return {
        "code": 200,
        "message": "用户更新成功"
    }

代码解释:

  • user_request 是用户传过来的新数据
  • user 是从数据库里查出来的旧对象
  • user.name = user_request.name 就是把旧对象的 name 改成新的 name
  • SQLAlchemy 会自动追踪对象的变化。等你提交的时候,它就知道哪些字段被改了,然后只更新那些字段
  • 同样不用手动写 commit(),依赖项会处理

更新用户运行结果:

四、关联关系

数据库里的表往往不是孤立的,它们之间会有关系。常见的关系有三种:一对一、一对多、多对多。

4.1 一对一(1:1)

一对一的意思就是:A 表的一条记录,只对应 B 表的一条记录。反过来也一样。

实现这种关系通常有两种办法:

第一种是主键关联。 把 A 表的主键,同时做成 B 表的外键,并且这个外键参考 B 表的主键。这样两条记录的主键是一样的,自然就一对一了。

第二种是外键关联。 在任意一张表里加一个外键字段,然后给这个外键加上唯一约束。这样每个外键值只能出现一次,也就保证了一对一。

4.2 1:M

一对多是最常见的关系。它的意思是:A 表的一条记录,可以对应 B 表的多条记录。但是 B 表的每一条记录,只能属于 A 表的一条记录。

比如: Department(部门)和 User(用户)就是一对多关系。一个部门可以有多个员工,但是一个员工只能属于一个部门。

4.2.1 怎么在模型里表达这种关系

你需要在"多"的那一方加一个外键。用户是"多"的一方,所以 User 模型里要加 department_id

python 复制代码
from sqlalchemy import ForeignKey

class User(Base):
    __tablename__ = "t_user"
    
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, name="user_id",
                                    comment="用户ID")
    name: Mapped[str] = mapped_column(String(20), nullable=False, name="user_name",
                                      comment="用户名称")
    password: Mapped[str] = mapped_column(String(20), nullable=False, name="user_password",
                                          comment="用户密码")
    salary: Mapped[float] = mapped_column(Float(6, 2), nullable=False, name="user_salary",
                                          comment="用户薪水")
    birthday: Mapped[datetime] = mapped_column(DateTime, nullable=False, name="user_birthday",
                                               comment="用户出生日期")
    department_id: Mapped[int] = mapped_column(ForeignKey("t_department.department_id"),
                                               nullable=False, name="department_id",
                                               comment="部门ID")

ForeignKey("t_department.department_id") 就是告诉数据库:这个字段的值,必须是 t_department 表里 department_id 字段已经存在的值。这样就建立了两张表之间的联系。

4.2.2 添加部门的同时添加用户

有时候你希望一次操作就完成两件事:先添加一个部门,然后给这个部门添加几个员工。

这里有一个关键点:部门插入数据库之后,它的 id 才会生成。你必须等这个 id 生成了,才能把它赋给员工的外键字段。

在异步 Session 里,你可以用 await session.flush() 来做到这一点。flush() 的作用是把 Session 里还没提交的数据,先发送到数据库执行一遍。这样部门就有了 id,但是事务还没真正提交。如果后面出错了,整个事务还是可以回滚的。

python 复制代码
class DepartmentRequest(BaseModel):
    name: str
    location: str

class DepartmentAddRequest(DepartmentRequest):
    user_list: List[UserRequest]

@app.post("/departments/")
async def add_department(
    department_add_request: DepartmentAddRequest,
    session: AsyncSession = Depends(get_session)
):
    # 判断传过来的数据是不是空的
    if department_add_request is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="部门信息不能为空"
        )

    if department_add_request.user_list is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="部门员工信息不能为空"
        )

    # 第一步:创建部门对象
    depart = Department(
        name=department_add_request.name,
        location=department_add_request.location
    )
    
    # 把部门加到 Session
    session.add(depart)
    
    # 先执行插入,让数据库生成部门 id
    await session.flush()

    # 第二步:遍历用户列表,给每个用户设置部门 id
    for user in department_add_request.user_list:
        user_object = User(
            name=user.name,
            password=user.password,
            salary=user.salary,
            birthday=user.birthday,
            department_id=depart.id  # 这里用到了刚生成的部门 id
        )
        session.add(user_object)

    # 最后统一提交
    await session.commit()

    return {
        "code": 200,
        "message": "部门添加成功"
    }

代码解释:

  • session.add(depart) 只是把部门放进了待办清单
  • await session.flush() 让数据库先执行插入操作,这样 depart.id 就有值了
  • 然后我们再遍历用户列表,把 depart.id 赋给每个用户的 department_id
  • 最后 await session.commit() 一次性提交所有改动。如果中间任何一步出错了,部门和用户都不会被真正写进数据库

4.2.3 根据部门名称查询该部门的员工

有时候你需要把两张表的数据拼在一起返回。比如你知道部门名称,想查出这个部门下所有员工的信息,同时还要带上部门名称和地址。

这就需要用到表连接(join)

python 复制代码
class UserDepartmentResponse(BaseModel):
    id: int
    name: str
    password: str
    salary: float
    birthday: datetime
    department_id: int
    department_name: str
    department_location: str

@app.get("/api/users/{name}")
async def get_users_by_department_name(
    name: str,
    session: AsyncSession = Depends(get_session)
):
    # 连接用户表和部门表,条件是用户的外键等于部门的主键
    stmt = Select(User, Department).join(
        Department, 
        User.department_id == Department.id
    ).where(Department.name == name)

    result = await session.execute(stmt)
    user_list = result.all()

    # 把查询结果组装成新的格式
    user_department_list = []
    for item in user_list:
        user_obj = item[0]        # 第一个是 User 对象
        department_obj = item[1]  # 第二个是 Department 对象

        user_department_response = UserDepartmentResponse(
            id=user_obj.id,
            name=user_obj.name,
            password=user_obj.password,
            salary=user_obj.salary,
            birthday=user_obj.birthday,
            department_id=user_obj.department_id,
            department_name=department_obj.name,
            department_location=department_obj.location
        )
        user_department_list.append(user_department_response)

    return {
        "code": 200,
        "message": "查询成功",
        "data": user_department_list
    }

代码解释:

  • Select(User, Department) 表示同时查两张表的数据
  • .join(Department, User.department_id == Department.id) 就是把两张表连起来。连接条件是用户的 department_id 等于部门的 id
  • .where(Department.name == name) 是筛选条件:只查指定名称的部门
  • result.all() 返回的是一个列表,列表里的每个元素是一个元组。元组里有两个对象:第一个是 User,第二个是 Department
  • 我们用 for 循环把这两个对象拆开,然后重新组装成 UserDepartmentResponse 返回给前端

4.2.4 根据用户名称查询所在部门

反过来,你也可以从用户出发,查出他所在的部门。这时候连接语句是类似的,只是 Select 里放的是 Department

python 复制代码
@app.get("/departments/{user_name}")
async def get_department_by_user_name(
    user_name: str,
    session: AsyncSession = Depends(get_session)
):
    stmt = Select(Department).join(
        User,
        Department.id == User.department_id
    ).where(User.name == user_name)

    result = await session.execute(stmt)
    department = result.scalar_one_or_none()

    return {
        "code": 200,
        "message": "查询成功",
        "data": department
    }

代码解释:

  • Select(Department) 表示我们只关心部门的信息
  • join(User, Department.id == User.department_id) 还是连接两张表
  • where(User.name == user_name) 根据用户名来筛选
  • scalar_one_or_none() 表示只取一个结果。因为一个用户只属于一个部门,所以结果最多只有一个

4.3 多对多(M:N)

多对多的意思是:A 表的一条记录可以对应 B 表的多条记录,同时 B 表的一条记录也可以对应 A 表的多条记录。

举个例子:一个学生可以选多门课,一门课也可以被多个学生选。学生和课程之间就是多对多关系。

实现多对多关系需要一张中间表。这张表只存两个外键:一个指向 A 表,一个指向 B 表。通过这张中间表,就把多对多关系拆成了两个一对多关系。

结语

增删改是基本功,关联关系是分水岭。当你能熟练驾驭flush与join,你就从简单的接口编写者,变成了能设计数据结构的工程师。

相关推荐
常常有4 小时前
中间件与依赖系统:构建高效 Web 后端的双重利器
开发语言·python·中间件·fastapi
.唉1 天前
06. FastAPI框架从入门到实战
python·fastapi·web
XerCis1 天前
ngrok实现内网穿透(以Python FastAPI为例)
开发语言·python·fastapi·ngrok
Li emily2 天前
用外汇实时api搭建多货币对波动率实时看板
python·api·fastapi
小李云雾2 天前
实际代码操作知识点分析:SQLAlchemy+FastAPI + 异步MySQL 全流程解析 + 增删改查逐行注释
数据库·mysql·fastapi
曲幽2 天前
初探:用 FastAPI 搭建你的第一个 AI Agent 接口
python·ai·llm·agent·fastapi·web·chat·httpx·ollama
高木木的博客2 天前
数字架构智能化测试平台(2)--AI DevOps测试流程框架
python·llm·fastapi·cicd
紫小米3 天前
后端日志管理
python·fastapi
L-影3 天前
常见的 ORM 工具
开发语言·数据库·fastapi·orm