(十五)事务、外键与连接生命周期
1. 一组修改作为一个整体提交
导入两本书时,希望两条都成功后一起保存;中间失败,就撤回这组修改。这个整体称为事务。commit() 提交,rollback() 回滚。Python 3.12 的 autocommit=False 让连接持续处于事务管理中,提交或回滚后再准备下一次事务。
# 错误传出 with connection 时回滚;在主体内部接住后正常退出则会提交。
from contextlib import closing
import sqlite3
with closing(sqlite3.connect(":memory:", autocommit=False)) as connection:
with connection:
connection.execute("CREATE TABLE books(title TEXT UNIQUE)")
connection.execute("INSERT INTO books VALUES (?)", ("Python",))
try:
with connection:
connection.execute("INSERT INTO books VALUES (?)", ("NumPy",))
connection.execute("INSERT INTO books VALUES (?)", ("Python",))
except sqlite3.IntegrityError:
print("已回滚")
print([row[0] for row in connection.execute("SELECT title FROM books ORDER BY rowid")])
with connection:
connection.execute("INSERT INTO books VALUES (?)", ("NumPy",))
try:
connection.execute("INSERT INTO books VALUES (?)", ("Python",))
except sqlite3.IntegrityError:
print("主体接住了错误")
print([row[0] for row in connection.execute("SELECT title FROM books ORDER BY rowid")])输出“已回滚”、['Python']、“主体接住了错误”、['Python', 'NumPy']。第二次 NumPy 的插入本来成功,后续唯一约束失败只撤销了失败的那条普通语句;异常又在主体内被处理,外层事务上下文正常退出,于是提交了 NumPy。
要让整批失败一起回滚,把这组修改放在同一个事务中,让失败传到事务边界之后再反馈。嵌套两个 with connection 不会自动创建两个独立事务;需要局部撤销时,SQLite 还提供 SAVEPOINT,后续较复杂的业务再据此划分。
2. 提交、回滚与关闭各有一个动作
| 操作 | 本章采用的连接模式中的动作 |
|---|---|
connect(..., autocommit=False) |
建立连接并启用显式事务管理 |
with connection: |
正常离开时提交,异常离开时回滚,不关闭连接 |
connection.commit() / rollback() |
结束当前事务,并准备下一次事务 |
connection.close() |
关闭连接;未提交的修改不会因关闭而自动提交 |
closing(connection) |
离开时调用 close(),不代替事务处理 |
autocommit=True 让 SQLite 按自动提交方式工作,此时 Python 的 commit()、rollback() 不负责提交一组待处理修改。Python 3.12 还保留旧的事务兼容模式;本章每次创建连接都明确选定模式,不依赖默认值猜行为。Python 事务控制
3. 外键开关在事务开始前设置
SQLite 的外键检查按连接开启,在已有事务中改 PRAGMA foreign_keys 不会切换检查状态。因此先用自动提交模式配置连接,再切到本章的事务模式:
# PRAGMA 是 SQLite 的配置与查询语句;外键检查先在事务外开启。
from contextlib import closing
import sqlite3
with closing(sqlite3.connect(":memory:", autocommit=True)) as connection:
connection.execute("PRAGMA foreign_keys = ON")
connection.autocommit = False
with connection:
connection.execute("CREATE TABLE books(id INTEGER PRIMARY KEY)")
connection.execute("CREATE TABLE logs(book_id INTEGER REFERENCES books(id) ON DELETE CASCADE)")
connection.execute("INSERT INTO books VALUES (1)")
try:
with connection:
connection.execute("INSERT INTO logs VALUES (99)")
except sqlite3.IntegrityError:
print("外键没有找到书籍")
with connection:
connection.execute("INSERT INTO logs VALUES (1)")
connection.execute("DELETE FROM books WHERE id = 1")
print(connection.execute("SELECT COUNT(*) FROM logs").fetchone()[0])输出“外键没有找到书籍”和 0。日志不能指向不存在的 99;后来删除书籍 1,级联动作一起删除它的日志。SQLite 外键
事务还要协调不同连接的读写。SQLite 同一时刻只有一个写事务,另一个写者可能等待或收到锁定错误;单条 UPDATE total = total + ? 把读取当前值和更新交给数据库执行,但用整份旧快照覆盖数据仍然可能丢失业务更新。并发章节会继续结合等待、重试和事务范围处理。SQLite 事务