SQLite RETURNING:拿到的值为什么还没包含 AFTER 触发器的修改

10-01 3阅读

插入成功后,到底读到了哪一版

应用插入一条记录,顺便用 RETURNING 取得编号和显示名,省掉一次查询。如果表里还有 AFTER INSERT 触发器会整理显示名,接口返回值可能与紧接着查询的表内值不同。排查这类问题时,先确认返回值属于哪一步,再检查事务是否提交,不能仅凭拿到了编号就推断最终状态。

下面在内存数据库创建一个最小例子:插入 alpha 与 beta,触发器把同一行的名称转成大写。SQL 功能要求 SQLite 3.35.0 或更新版本;本次实际验证环境是 Python 3.12.14、SQLite 3.53.1。Python 自身版本不能代替底层 SQLite 版本检查,代码开头同时设置了明确的最低条件。

同一连接里读出两种结果

RETURNING 对本次主语句直接修改的行产生结果,其列值不包含随后 AFTER 触发器做出的修改。要注意,“取值所反映的阶段”和“结果交给应用的时刻”并不相同:SQLite 会先完成本语句的修改动作,再交出返回结果,并不是应用读完第一行才去执行触发器。

代码先明确开启事务,插入后用 fetchall 取尽返回行,再查询表内实际内容。返回行没有顺序保证,所以比较时在 Python 中按编号排序;查询表内记录则显式使用 ORDER BY。这样测试检验的是值的差异,不会意外依赖某次执行刚好出现的行顺序。

SQLite RETURNING:拿到的值为什么还没包含 AFTER 触发器的修改

AI概念示意图:主语句记录的返回值与触发器处理后的表内值分成两张结果卡。图片用于解释概念,不是运行截图。

import sqlite3

assert sqlite3.sqlite_version_info >= (3, 35, 0)
con = sqlite3.connect(":memory:", isolation_level=None)
try:
    con.executescript("""
        CREATE TABLE item(id INTEGER PRIMARY KEY, label TEXT NOT NULL);
        CREATE TRIGGER normalize_label AFTER INSERT ON item
        BEGIN
            UPDATE item SET label = upper(NEW.label) WHERE id = NEW.id;
        END;
    """)
    con.execute("BEGIN")
    cursor = con.execute(
        "INSERT INTO item(label) VALUES (?), (?) RETURNING id, label",
        ("alpha", "beta"),
    )
    returned = sorted(cursor.fetchall())
    stored = con.execute("SELECT id, label FROM item ORDER BY id").fetchall()
    assert returned == [(1, "alpha"), (2, "beta")]
    assert stored == [(1, "ALPHA"), (2, "BETA")]
    print("returned:", returned)
    print("stored:", stored)
    assert con.in_transaction
    con.execute("ROLLBACK")
    remaining = con.execute("SELECT count(*) FROM item").fetchone()[0]
    assert remaining == 0
    print("after rollback:", remaining)
finally:
    con.close()

编号已经返回,事务仍能回滚

输出第一行保留小写,第二行已经是大写,说明同一连接查询到了触发器处理后的版本。最后回滚后计数为零,证明先前拿到的两个编号并不代表事务已经提交。本例主动回滚是为了展示边界;正式业务应在事务成功提交后,再把“已经保存”的承诺交给调用方。

如果界面必须展示触发器修改后的字段,可以先收集返回的主键,再在同一事务内按这些主键查询最终字段。后续仍应按应用约定处理提交失败。若只需要数据库分配的编号,而触发器不修改编号,RETURNING 很合适,但接口最好只返回真正需要的列,减少误解和临时数据量。

批量操作还要检查结果规模

RETURNING 的输出会在数据库执行期间暂存在内存中,改很多行又返回大文本时,内存需求可能明显增加。逐行从游标读取可以减少应用侧一次性保存的数据,却不能据此认为数据库也没有暂存整批返回结果。本文只有两条短文本,所以 fetchall 便于完整对照。

Python 的 executemany 会丢弃生成的结果行,不能直接把带 RETURNING 的语句交给它就期待得到全部编号。需要这些结果时,应选择明确可读取结果的执行方式,并保持事务边界。测试至少同时核对返回内容、再次查询内容和提交或回滚后的状态,三者分别回答不同问题。把这三项一起保留在回归用例里,有助于发现后来新增触发器带来的变化。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。