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 秒实验
- 把第一次 commit 注释掉,直接再查一次,看能不能查到那一行。
- 在 flush 之后不 rollback 直接 commit,看那一行会不会留下。
- 想一想:为什么「改一个字段」不需要写 UPDATE 语句?
常见误区
- add 之后就以为存好了:没 commit,这个请求里能查到,换一个请求就没了。
- 捕获 IntegrityError 却不 rollback:后面所有查询都报错,排查方向会完全跑偏。
- 以为 rollback 只撤最后一条:它撤的是整个事务,前面攒的都白干。
- 对象脱离会话后再改属性:commit 管不到它,改动静默丢失,不报错。
自测三问(点开对答案)
1. add 之后为什么 id 还是 None? —— id 由数据库生成,SQL 还没发出去。 2. flush 和 commit 差在哪? —— flush 发 SQL 但不开事务边界,能撤销,别的事务看不见。 3. 捕获 IntegrityError 后第一件事做什么? —— rollback,否则会话一直处于失败待回滚状态。数据层通了,下一篇开始写页面:表单类、端点、模板三件套。
- ☐ (本篇完,下一篇 2.6 CRUD 之一:新增与列表)