文章目录
- 前言
- 一、单表的增加操作
- 二、单表的删除操作
- 三、单表的更新操作
- 四、关联关系
-
- [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.name、user.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,你就从简单的接口编写者,变成了能设计数据结构的工程师。