🧪 第2章 建立数据库应用程序
📄 一句话概括

add 只是把对象挂进会话的待办清单,commit 之前库里什么都没有。 改字段不用写 UPDATE:对象在会话里,commit 时 SQLAlchemy 自己比对旧值。 出错之后必须 rollback,否则会话一直处于「失败待回滚」状态。

📝 本章和前面几章的关系

这一篇对应教材 2.4.3:SQLAlchemy 基本的资料操作。 上一篇把表建好了,这一篇把增删改查和事务讲完,下一篇开始写页面。

一、add 与 commit 是两步

u1 = User(name="张三", email="[email protected]")
db.session.add(u1)
print(u1.id)      # None
db.session.commit()
print(u1.id)      # 1

实测就是这样:add 之后 id 是 None,commit 之后才是 1。 因为 id 由数据库生成,SQL 没发出去之前它根本不存在。 这句话值得记:add 只登记意图,commit 才发 SQL。

没 commit 就去查,查不到——数据只在这个会话的内存里,别的事务更看不见。

二、批量插入

db.session.add_all([
    User(name="李四", email="[email protected]"),
    User(name="王五", email="[email protected]"),
    User(name="赵六", email=None),
])
db.session.commit()

实测四行进去,总行数 4。 add_all 写起来是省事的,但它到底发几条 SQL,取决于版本与驱动。 SQLAlchemy 2.x 有批量插入优化,不一定一条一条发。 真要压插入速度,用 Core 的 insert 构造一次多行插入,那是另一个话题。 这一篇只求「知道有这回事」。

三、三种查询写法

实测三种写法拿到同一个对象:<User 1 张三>。

写法 出处 现在还用吗
User.query.filter_by() Flask-SQLAlchemy 老接口 教材用,仍可用
session.query(User).filter() SQLAlchemy 1.x 风格 可用
db.select(User).where() SQLAlchemy 2.0 风格 本课统一用

三种都能跑,区别在风格与未来兼容性。 本课一律用第三种,旧写法只在对照时出现。这样读官方文档不会对不上。 教材用的是第一种,看到 User.query 不要以为写错了——它是 Flask-SQLAlchemy 独有的语法糖, 脱离 Flask 就没有了。

四、改与删都不写 SQL

改字段只有两句:取出对象,改属性,commit。没有 update 语句。 实测把「张三」改成「张三丰」,再查就是新的值。 原理:对象在会话里,commit 时 SQLAlchemy 把当前值和加载时的快照比一遍,只发有变化的列。

删是 session.delete(obj),同样要 commit 才生效。 这是 ORM 相对裸 SQL 唯一真正省事的地方:不用记哪张表哪些列改过。

五、flush、commit、rollback 三件事

动作 发 SQL 吗 能撤销吗 别的事务看得见吗
flush 发 能 看不见
commit 发 不能 看得见
rollback 撤 — 回到起点

实测 flush 之后同一个事务里能查到那一行,行数从 3 变成 4;rollback 之后又回到 3。 这就是「页面报错了但数据还在」的来源:多半是 except 里没 rollback,后面某条路径又把会话提交了。 rollback 会把会话里所有未提交的改动一起丢掉,包括你以为已经存好的那些。 它不是「只撤最后一条」。

六、唯一约束撞上去会怎样

实测重复的 email 报 IntegrityError: UNIQUE constraint failed: user.email。 捕获之后第一件事是 rollback。不 rollback 的话,会话一直处于失败状态。 那时后面每一条 SQL 都报同样的错,看起来像「数据库挂了」,其实只是会话脏了。

这个现象值得记住:一次没 rollback,后面的报错全是假的。

七、动手 Demo:SQLAlchemy 基本数据操作

这个 Demo 把增、批、查、改、删、回滚、撞约束七件事各跑一遍。 每一节都打印真实结果,id 什么时候出现、行数什么时候变,全看得见。

u1 = User(name="张三", email="[email protected]")
db.session.add(u1)
db.session.commit()

# 2.0 风格的查询
row = db.session.execute(
    db.select(User).where(User.name == "张三")).scalar_one()

row.name = "张三丰"
db.session.commit()

实跑输出:

--------------------------------------------------------------------
SQLAlchemy 基本数据操作
--------------------------------------------------------------------

  ① 增:add 与 commit 是两步
  --------------------------------------------------------------------
  add 之后,id 是 -> None
  commit 之后,id 是 -> 1
  ! add 只是把对象挂进会话的待办清单,commit 才发 SQL。没 commit 就查,查不到——数据只在内存里。

  ② 批量插入
  --------------------------------------------------------------------
  总行数 -> 4

  ③ 查:三种写法,一个结果
  --------------------------------------------------------------------
  User.query(旧写法)-> <User 1 张三>
  db.select(新写法)-> <User 1 张三>
  session.query(中间写法)-> <User 1 张三>
  ! 三种都还能用,但教材用的是第一种、官方推荐第二种。
  新写法在 2.x 里是主流,本课一律用它,旧写法只在对照时出现。

  ④ 改与删
  --------------------------------------------------------------------
  改完再查 -> 张三丰
  删完总行数 -> 3
  ! 改字段不需要 update 语句:对象在会话里,commit 时它自己比对旧值生成 UPDATE。

  ⑤ 回滚:commit 之前都算没发生
  --------------------------------------------------------------------
  flush 之后同一事务里查得到 -> 4
  rollback 之后 -> 3(原来 3)
  ! flush 把 SQL 发出去但不开事务边界,rollback 能撤。
  这解释了为什么「页面报错了但数据还在」——多半是 except 里没 rollback。

  ⑥ 唯一约束撞上了会怎样
  --------------------------------------------------------------------
  IntegrityError: UNIQUE constraint failed: user.email
  ! 捕获之后必须 rollback,否则会话一直处于「失败待回滚」状态,后面每一条 SQL 都会报同样的错。

第一节那两个 id,一个是 None 一个是 1,这一篇的一半内容就在这两行里。 第五节的行数 3 到 4 再到 3,是 flush 与 rollback 唯一的硬证据。 第六节捕获异常后那一句 rollback 不能省,省了后面每条 SQL 都报错。

30 秒实验

  1. 把第一次 commit 注释掉,直接再查一次,看能不能查到那一行。
  2. 在 flush 之后不 rollback 直接 commit,看那一行会不会留下。
  3. 想一想:为什么「改一个字段」不需要写 UPDATE 语句?

常见误区

  1. add 之后就以为存好了:没 commit,这个请求里能查到,换一个请求就没了。
  2. 捕获 IntegrityError 却不 rollback:后面所有查询都报错,排查方向会完全跑偏。
  3. 以为 rollback 只撤最后一条:它撤的是整个事务,前面攒的都白干。
  4. 对象脱离会话后再改属性:commit 管不到它,改动静默丢失,不报错。
自测三问(点开对答案) 1. add 之后为什么 id 还是 None? —— id 由数据库生成,SQL 还没发出去。 2. flush 和 commit 差在哪? —— flush 发 SQL 但不开事务边界,能撤销,别的事务看不见。 3. 捕获 IntegrityError 后第一件事做什么? —— rollback,否则会话一直处于失败待回滚状态。

数据层通了,下一篇开始写页面:表单类、端点、模板三件套。

相关笔记