学会 SQLAlchemy 搭建 Python 与 MySQL 之间的数据访问桥梁 🚀

3 阅读16分钟

新增怎么拿 ID?删除为什么 flush?修改后一定要 refresh 吗? 从单条操作到批量处理,直接看需求、代码和每一步的作用。

先看公共代码:提交、回滚、关闭统一处理,业务里不重复写。

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 需求:注册用户,并返回编号、姓名和创建时间

流程:创建对象 → 登记新增 → 执行写入 → 按需重读 → 准备返回结果。

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 写订单 → 两条记录一起保存。

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。

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 需求:把表格中的客户名单批量导入数据库

流程:把表格数据整理成字典 → 批量写入 → 等待统一提交。

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 → 写入商品明细 → 一起保存。

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 → 准备结果。

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 需求:删除指定用户,不存在就提示

流程:查用户 → 登记删除 → 按需提前执行 → 公共代码提交。

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 → 限定范围 → 执行删除。

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 删除用户,但不能删除自己

流程:检查目标 → 执行删除 → 等统一提交。

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 需求:修改昵称,并返回更新后的用户信息

流程:查用户 → 改属性 → 执行更新 → 按需重读 → 准备结果。

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 → 筛出待审核账号 → 更新状态。

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 需求:只改本次填写的资料,没填写的保持原样

流程:取本次字段 → 查用户 → 逐项赋值 → 准备结果。

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() 保存到数据库。

不用再 add,也不用强行 flush、refresh。 本例直接读取已赋的值,公共代码 commit 时会执行更新。

3.4 需求:旧密码正确,才允许修改新密码

流程:查用户 → 核对旧密码 → 生成新密码摘要 → 更新。 verify_password、hash_password 是已有的密码校验、摘要工具。

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 查看详情,账号异常就提示

流程:按主键查询 → 判断结果 → 提取需要展示的字段。

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 需求:登录时,按用户名找到账号

流程:写条件 → 查询 → 取一个用户 → 判断存在。

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 需求:前端传哪些条件,就按哪些条件筛选用户

流程:读取参数 → 按需添加条件 → 执行查询 → 返回列表。

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 统计数量。

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 需求:用户列表分页,同时返回总条数

流程:统计总数 → 取一页 → 整理返回数据。

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 是模型类,分别代表员工表和部门表。 只保留本例需要的字段:

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,联表就是按这两个编号找到对应关系。假设表里已有这些数据:

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。

想继续查用法,可以收藏这些官方参考:

💬 哪里没看懂,或者想看哪类例子,欢迎留言提出问题,也欢迎补充和指正。

🔄 本文会根据反馈持续更新,继续补充常见需求和更容易理解的示例,欢迎收藏备用。