新增怎么拿 ID?删除为什么 flush?修改后一定要 refresh 吗? 从单条操作到批量处理,直接看需求、代码和每一步的作用。
先看公共代码:提交、回滚、关闭统一处理,业务里不重复写。
python
try:
# 这里先完成本次新增、删除或修改
db.commit() # 先执行还没写入的变化,再确认保存本次全部操作
except Exception:
db.rollback() # 出错就撤销本次还没提交的改动
raise
finally:
db.close() # 用完归还连接;关闭本身不会保存数据
- commit:最终确认保存,内部会先自动 flush。
- rollback:撤销本次未提交的改动,不能撤销已经提交的事务。
- close:释放资源,纯查询结束也要关闭。
下面的业务代码放在 try 中执行,提交成功后再返回 response。 response 是提前取好的普通数据,db 关闭后也能用。纯查询不需要 commit;flush 失败也要 rollback。
一、增:新增用户、关联数据和批量导入 ➕
本类总结: 单条用 add,多条用 add_all;提前拿 ID 用 flush,需要重读才用 refresh。最后由公共代码 commit 保存。
1.1 需求:注册用户,并返回编号、姓名和创建时间
流程:创建对象 → 登记新增 → 执行写入 → 按需重读 → 准备返回结果。
python
user = User(name="小林", email="lin@example.com") # ① 先把用户信息放进 user,还没写进数据库
db.add(user) # ② 告诉 db:这个用户等着新增,通常还没真正写入
db.flush() # ③ 先写进数据库,拿到新用户的 ID;还没最终提交
db.refresh(user) # ④ 从数据库再查一遍,更新 user 里的值,读到自动生成的创建时间
response = { # ⑤ 先把要返回的内容装进字典,db 关闭后也能用
"id": user.id,
"name": user.name,
"created_at": user.created_at.isoformat(),
}
# 最后由公共文件 commit,确认保存成功后再返回 response
- 用 add:告诉 db,这个用户需要新增。
- 用 flush:先把用户写进去,才能在提交前拿到新 ID。
- 用 refresh:因为这里要返回数据库自动生成的创建时间,所以再查一次,把它读回 user;如果 flush 后已经拿到这个值,就可以省略。
1.2 需求:注册用户,同时给他创建第一笔订单
流程:先写用户拿 ID → 用 ID 写订单 → 两条记录一起保存。
python
user = User(name="小周", email="zhou@example.com")
db.add(user) # ① 告诉 db:准备新增这个用户
db.flush() # ② 先写入用户,拿到他的 ID,下面创建订单要用
order = Order(user_id=user.id, amount=99)
db.add(order) # ③ 告诉 db:还要新增这笔订单,订单属于刚创建的用户
db.flush() # ④ 先写入订单,拿到订单 ID,等会儿一起返回
response = {"user_id": user.id, "order_id": order.id}
# 两次 flush 都没最终提交;公共文件统一保存,中途失败就一起撤销
- 用 add:用户和订单都是新数据,都要交给 db 新增。
- 用 flush:第一次拿用户 ID,方便创建订单;第二次拿订单 ID,方便返回结果。
- 最后一起 commit:让用户、订单一起保存,中途失败就一起撤销。
1.3 需求:后台批量添加员工账号,并返回各自的编号
流程:准备多个对象 → 一次登记 → 执行插入 → 收集 ID。
python
users = [
User(name="小陈", email="chen@example.com"),
User(name="小王", email="wang@example.com"),
]
db.add_all(users) # ① 告诉 db:这几个用户都要新增,不用一个个 add
db.flush() # ② 把这些用户写进去,拿到各自的 ID;底层可能执行多条 SQL
response = [{"id": user.id, "name": user.name} for user in users]
- 用 add_all:同时新增多个用户,不用反复写 add。
- 用 flush:先完成插入,拿到每个人的 ID,再整理返回结果。
1.4 需求:把表格中的客户名单批量导入数据库
流程:把表格数据整理成字典 → 批量写入 → 等待统一提交。
python
data = [
{"name": "客户甲", "email": "client-a@example.com"},
{"name": "客户乙", "email": "client-b@example.com"},
]
statement = insert(User) # ① 准备好"往用户表加数据"的语句,现在还没写入
db.execute(statement, data) # ② 把这批数据写进去,name、email 对应 User 的属性
response = {"count": len(data)} # 全部写入并提交成功后,再返回新增了多少条
- 用 insert:告诉数据库,这次要新增用户数据。
- 用 execute:把这批字典数据交给数据库执行,不必逐个创建 User 对象。
1.5 需求:创建订单,同时保存订单里的商品
流程:创建订单 → 拿订单 ID → 写入商品明细 → 一起保存。
python
order = Order(user_id=1, amount=99) # ① 为已有用户创建订单,本例金额用整数演示
db.add(order) # ② 告诉 db:准备新增订单
db.flush() # ③ 先写入订单,拿到 order.id,下面的明细要用
items = [
OrderItem(order_id=order.id, product_id=101, quantity=1),
OrderItem(order_id=order.id, product_id=102, quantity=2),
]
db.add_all(items) # ④ 把两条商品明细交给 db,关联刚创建的订单
response = {"order_id": order.id}
# 公共文件统一提交,订单和明细一起成功;任一步失败就一起撤销
- 用 add:新增订单。
- 用 flush:先拿到订单 ID,商品明细才能记录自己属于哪个订单。
- 用 add_all:同时新增多条商品明细,不用逐条 add。
- 最后一起 commit:避免出现订单已保存、明细却没保存的情况。
1.6 需求:新增用户前,用户名重复就提示
流程:查重 → 新增 → 拿 ID → 准备结果。
python
username = "xiaolin" # ① 用户填写的账号
statement = select(User).where(User.username == username)
user = db.execute(statement).scalar_one_or_none() # ② 查有没有同名账号
if user is not None:
raise ValueError("用户名已存在")
user = User(username=username, name="小林", email="lin@example.com")
db.add(user) # ③ 没重名,把新用户交给 db
db.flush() # ④ 先写入,拿到新 ID;最后仍由公共代码提交
response = {"id": user.id, "username": user.username}
- 用 execute:执行查询,拿到查询结果。
- 用 scalar_one_or_none:从结果里取一个用户;找到一条返回用户对象,没找到返回 None,找到多条则报错。
- 用 add + flush:登记新增,再先拿到 ID;本例不需要 refresh。
用户名还要有数据库唯一约束,防止两人同时注册出同名账号。这里只演示用户数据写入,密码处理另行完成。
二、删:删除指定记录和批量删除 🗑️
本类总结: 按对象删用 db.delete(user),按条件删用 delete(User) 配合 execute。flush 提前执行,commit 最终保存。
软删除 :不真正删除记录,只把 is_deleted 这样的字段改为 True,后续查询时要过滤掉它,需要时可改回标记恢复。硬删除:执行 DELETE,提交后删除记录;下面的例子都属于硬删除。
2.1 需求:删除指定用户,不存在就提示
流程:查用户 → 登记删除 → 按需提前执行 → 公共代码提交。
python
user = db.get(User, 3) # ① 找 ID 为 3 的用户,找不到就是 None
if user is None:
raise ValueError("用户不存在")
db.delete(user) # ② 登记:这个用户要删除,通常还没执行 DELETE
db.flush() # ③ 可选:现在执行 DELETE,提前发现外键等错误;还没提交
response = {"deleted": True}
# 公共代码随后 commit,确认保存这次删除
- 用 get:先确认用户存在,找不到就提示。
- 用 delete:登记要删除哪个用户。
- 用 flush(可省略):提前执行删除,比如用户还有关联订单、数据库不允许删除时,可以在这里提前发现错误。
本例删完就结束,通常可以省略 flush ,公共代码的 commit 会自动执行删除并提交。保留它只是为了演示:flush 先执行,commit 才确认保存;提交前仍可回滚。
2.2 需求:后台勾选多个用户,一次批量删除
流程:拿到选中的 ID → 限定范围 → 执行删除。
python
user_ids = [3, 4] # ① 页面上勾选了哪几个用户,就传哪些 ID
statement = delete(User).where(User.id.in_(user_ids)) # ② 只删除这些 ID 对应的用户
db.execute(statement) # ③ 执行删除,等待公共文件提交
response = {"message": "批量删除完成"} # 提交成功后才返回
- 用 in_:匹配列表中的多个用户 ID。
- 用 delete + where:限定要删除哪些用户,避免误删其他人。
- 用 execute:一次执行删除,不用逐个查出用户再删。
有关联订单等数据时,先确定数据库外键和删除规则;批量删除不执行 ORM 的逐对象关系级联。
2.3 需求:按 ID 删除用户,但不能删除自己
流程:检查目标 → 执行删除 → 等统一提交。
python
user_id, current_user_id = 3, 1 # ① 要删的用户 ID、当前登录用户 ID
if user_id == current_user_id:
raise ValueError("不能删除自己")
statement = delete(User).where(User.id == user_id) # ② 只删除指定用户
db.execute(statement) # ③ 现在执行 DELETE,还没最终提交
response = {"deleted": True}
- 用 delete + where:直接说明删谁,不用先查出对象。
- 用 execute:已经执行删除,不必为了这条语句再 flush。
本例约定目标不存在也算成功;需要提示不存在时,用前面的 get + delete 写法。
三、改:修改单条、批量更新和修改密码 ✏️
本类总结: 已查出的对象直接改属性,不用再 add;批量修改用 update + where + values 配合 execute。需要重读时先 flush 再 refresh,最后统一 commit。
3.1 需求:修改昵称,并返回更新后的用户信息
流程:查用户 → 改属性 → 执行更新 → 按需重读 → 准备结果。
python
current_user = db.get(User, 1) # ① 找到 ID 为 1 的用户,db 会记住这个对象
if current_user is None:
raise ValueError("用户不存在")
current_user.name = "新昵称" # ② 直接改昵称,db 会记住,不用再 add 一次
db.flush() # ③ 把新昵称写进数据库,但还没最终提交
db.refresh(current_user) # ④ 从数据库再查一遍,更新 current_user 里的值;需要时才做
response = {"id": current_user.id, "name": current_user.name}
- 用 get:先找到要改昵称的用户。
- 直接给 name 赋值:db 会记住这个对象的修改,不用再 add。
- 用 flush:先把新昵称写进数据库;不急着执行时可等公共代码 commit。
- 用 refresh:本例演示重新读取更新后的字段,不需要重读时可省略。
3.2 需求:后台勾选几个账号,批量审核通过
流程:拿到选中的 ID → 筛出待审核账号 → 更新状态。
python
user_ids = [1, 2] # ① 本次勾选的账号
statement = (
update(User)
.where(User.id.in_(user_ids), User.status == "pending") # ② 只改选中的待审核账号
.values(status="approved") # ③ 把状态改为已审核
)
db.execute(statement) # ④ 一次更新这些账号,等待公共文件统一提交
response = {"message": "审核完成"}
- 用 update:说明这次要修改用户数据。
- 用 where + in_:限定本次勾选的待审核账号,其他用户不受影响。
- 用 values:说明状态要改成 approved。
- 用 execute:把修改语句交给数据库,一次完成更新。
这种批量更新的 ORM 处理过程与逐个修改对象不同。
3.3 需求:只改本次填写的资料,没填写的保持原样
流程:取本次字段 → 查用户 → 逐项赋值 → 准备结果。
python
changes = {"name": "新昵称"} # ① 只放本次提交、允许修改的字段;这里没改邮箱
if not changes:
raise ValueError("请至少填写一项资料")
user = db.get(User, 1) # ② 找到要修改的用户
if user is None:
raise ValueError("用户不存在")
for field, value in changes.items():
setattr(user, field, value) # ③ 本次只会修改 name,原来的 email 保留
response = {"id": user.id, "name": user.name, "email": user.email}
- 用 get:取得由当前 db 管理的用户对象。
- 用 setattr :按字段名给对象赋值,相当于
user.name = "新昵称"。 它固定接收 3 个参数 :- 第 1 个参数
user:要修改的对象,也就是查出来的用户。 - 第 2 个参数
field:要修改的字段名,必须是字符串,例如"name"。 - 第 3 个参数
value:要写入的新值,例如"新昵称"。 所以setattr(user, "name", "新昵称")就等于user.name = "新昵称"。它只是修改内存中的对象,最后仍由公共代码commit()保存到数据库。
- 第 1 个参数
不用再 add,也不用强行 flush、refresh。 本例直接读取已赋的值,公共代码 commit 时会执行更新。
3.4 需求:旧密码正确,才允许修改新密码
流程:查用户 → 核对旧密码 → 生成新密码摘要 → 更新。 verify_password、hash_password 是已有的密码校验、摘要工具。
python
user = db.get(User, 1) # ① 找到要修改密码的用户
if user is None:
raise ValueError("用户不存在")
if not verify_password("old-password", user.password): # ② 旧密码不对就停止
raise ValueError("旧密码错误")
password_hash = hash_password("new-password") # ③ 先处理新密码,不直接保存明文
statement = (
update(User)
.where(User.id == user.id) # ④ 只修改这个用户
.values(password=password_hash) # ⑤ 指定要保存的新值
)
db.execute(statement) # ⑥ 执行 UPDATE,等待公共代码统一提交
response = {"message": "密码修改成功"}
- 用 get:读出原密码摘要,先做校验。
- 用 update + where + values:说明改谁、哪个字段、改成什么。
- 用 execute:执行更新;只返回成功消息,无需再 flush 或 refresh。
四、查:查详情、条件查询和分页统计 🔍
本类总结: 按 ID 用 get,按条件用 select + where,多表联查用 join;取一个用户用 scalar_one_or_none,取用户列表用 scalars().all()。纯查询不用 commit。
4.1 需求:按用户 ID 查看详情,账号异常就提示
流程:按主键查询 → 判断结果 → 提取需要展示的字段。
python
user = db.get(User, 1) # ① 找 ID 为 1 的用户;get 用的是 ID,不是姓名或邮箱
if user is None:
raise ValueError("用户不存在")
if user.status == "disabled": # 本例约定 disabled 表示禁用
raise ValueError("用户已被禁用")
response = { # ② 先把要展示的内容装进字典,db 关闭后也能用
"id": user.id,
"name": user.name,
"email": user.email,
}
- 用 get:已经知道用户 ID,直接按 ID 找一个用户;找不到就是 None。
- 把字段装进字典:先取出要展示的数据,关闭 db 后也能使用。
这是纯查询,不需要 commit。
4.2 需求:登录时,按用户名找到账号
流程:写条件 → 查询 → 取一个用户 → 判断存在。
python
username = "xiaolin" # ① 用户填写的账号
statement = select(User).where(User.username == username)
user = db.execute(statement).scalar_one_or_none() # ② 找到是 User,没找到是 None
if user is None:
raise ValueError("用户不存在或密码错误")
response = {"id": user.id, "username": user.username} # ③ 这里只展示账号查询结果
- 用 select + where:按用户名查询,get 只能按主键查。
- 用 scalar_one_or_none:只允许零个或一个结果,多个会报错。
查到用户还不等于登录成功,登录流程还需验证密码和账号状态。
4.3 需求:前端传哪些条件,就按哪些条件筛选用户
流程:读取参数 → 按需添加条件 → 执行查询 → 返回列表。
python
params = {"name": "林"} # ① 模拟前端参数:本次只传姓名,也可以传 status
statement = select(User) # ② 先准备查询用户,暂时不加筛选条件
if params.get("name"): # ③ 传了姓名关键词,并且不为空,才按姓名筛选
statement = statement.where(User.name.contains(params["name"]))
if params.get("status") is not None: # ④ 传了状态才筛选;没传时 get 返回 None
statement = statement.where(User.status == params["status"])
result = db.execute(statement) # ⑤ 执行最终加好条件的查询
users = result.scalars().all() # ⑥ 取出用户对象列表,没查到就是 []
response = [{"id": user.id, "name": user.name} for user in users]
- 用 where :有条件才添加,并用
statement =接住新语句;多次添加表示条件要同时满足。 - 用 contains:按姓名中的一部分文字查找,例如"林"能找到"小林"。
- 用 execute:执行最终的查询语句。
- 用 scalars().all():把结果取成用户对象列表。
只传姓名就按姓名查,只传状态就按状态查,两个都传就一起筛选;都不传就不加条件。
4.4 需求:只查用户名单,并统计用户总数
流程:只选需要的列 → 读取行数据;另用 COUNT 统计数量。
python
rows = db.execute(select(User.id, User.name)).all() # ① 每个用户只取 id 和 name,两个值都会保留
user_list = [{"id": row.id, "name": row.name} for row in rows]
statement = select(func.count()).select_from(User) # ② 让数据库数一数,一共有多少个用户
count = db.scalar(statement) # ③ 把那个数字拿出来,这里就是用户总数
response = {"users": user_list, "total": count}
- 用 select(User.id, User.name):只查名单需要的编号和姓名。
- 用 execute(...).all():保留每行的两列数据;scalars 只保留第一列,这里不合适。
- 用 func.count + scalar:让数据库统计总数,再把这个数字拿出来。
4.5 需求:用户列表分页,同时返回总条数
流程:统计总数 → 取一页 → 整理返回数据。
python
page, page_size = 1, 10 # ① 第一页,每页 10 条;由请求传入正整数
total = db.scalar(select(func.count()).select_from(User)) # ② 统计全部用户数量
statement = (
select(User)
.order_by(User.id.desc()) # ③ ID 大的排前面,固定分页顺序
.offset((page - 1) * page_size) # ④ 第一页跳过 0 条,第二页跳过 10 条
.limit(page_size) # ⑤ 这一页最多拿 10 条
)
users = db.scalars(statement).all() # ⑥ 直接取出本页用户对象列表,没找到是 []
response = {
"items": [{"id": user.id, "name": user.name} for user in users],
"total": total,
"page": page,
"page_size": page_size,
}
- 用 count + scalar:让数据库统计,再取出那个数字。
- 用 order_by + offset + limit:固定顺序、跳过前面的记录、限制条数。
- 用 scalars + all:直接得到用户对象列表。
增加筛选条件时,统计总数和列表要使用同样的条件。
4.6 需求:员工列表中显示部门名称(多表联查)
Employee、Department 是模型类,分别代表员工表和部门表。 只保留本例需要的字段:
python
from sqlalchemy import Column, ForeignKey, Integer, String, select
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Department(Base): # 部门表
__tablename__ = "departments"
id = Column(Integer, primary_key=True)
name = Column(String(50)) # 部门名称
class Employee(Base): # 员工表
__tablename__ = "employees"
id = Column(Integer, primary_key=True)
name = Column(String(50)) # 员工姓名
department_id = Column(Integer, ForeignKey("departments.id")) # 所属部门的 ID
例如,员工"小林"的 department_id 是 1,部门"技术部"的 id 也是 1,联表就是按这两个编号找到对应关系。假设表里已有这些数据:
python
statement = (
select(Employee, Department) # ① 一起查询员工和部门
.join(Department, Employee.department_id == Department.id) # ② 按部门 ID 对应起来
)
rows = db.execute(statement).all() # ③ 每条结果里都有员工和他的部门
response = []
for employee, department in rows: # ④ 分别取出这两个对象
response.append({"name": employee.name, "department": department.name})
示例结果: [{"name": "小林", "department": "技术部"}]
- 用 select:选择要查询的模型。
- 用 join:按部门 ID 关联,返回能匹配到部门的员工。
- 用 execute:执行查询。
- 用 all:取出结果列表,每行包含员工、部门两个对象。
五、总结与参考 📌
add 先登记,flush 先执行,commit 才确认保存,refresh 按需重读。 出错用 rollback 撤销未提交的改动,用完 close 释放资源。先看需求,再选对应的 API。
想继续查用法,可以收藏这些官方参考:
- Session 基础:了解提交、回滚、flush 和 refresh。
- Session API:查具体方法的参数和返回值。
- ORM 查询指南:学习条件查询、多表联查和结果处理。
- ORM 增删改指南:查看新增、更新、删除及批量操作。
💬 哪里没看懂,或者想看哪类例子,欢迎留言提出问题,也欢迎补充和指正。
🔄 本文会根据反馈持续更新,继续补充常见需求和更容易理解的示例,欢迎收藏备用。