SQLite 外键已经声明,为什么还能插入无效数据?

举报
yd_235245466 发表于 2026/11/03 05:21:26 2026/11/03
【摘要】 在 SQLite 中,表里写了 REFERENCES,不代表每个连接都会执行外键检查。连接上的设置不同,同一张表就可能同时接受和拒绝无效引用。还有一个容易忽略的地方:事务中执行 PRAGMA foreign_keys = ON 不会报错,但也不会改变设置。如果 SQLite 编译时没有包含外键支持,或者读回的设置不是 1,则不能依赖外键约束。下面用两个连接演示这个区别。实测环境为 Windo...

在 SQLite 中,表里写了 REFERENCES,不代表每个连接都会执行外键检查。连接上的设置不同,同一张表就可能同时接受和拒绝无效引用。还有一个容易忽略的地方:事务中执行 PRAGMA foreign_keys = ON 不会报错,但也不会改变设置。如果 SQLite 编译时没有包含外键支持,或者读回的设置不是 1,则不能依赖外键约束。

下面用两个连接演示这个区别。实测环境为 Windows 11、Python 3.13.17 和 SQLite 3.50.4;程序只用 Python 标准库和内存数据库,不联网,也不创建数据库文件。代码使用的 sqlite_errorcode 和 sqlite_errorname 属性需要 Python 3.11 或更高版本;下列结果仅在上述环境实测。

把代码保存为 sqlite_foreign_keys_demo_20261103.py,用已经配置好的 Python 运行:

python -B sqlite_foreign_keys_demo_20261103.py
import sqlite3

DB = "file:foreign_key_demo?mode=memory&cache=shared"
a = sqlite3.connect(DB, uri=True, isolation_level=None)
b = sqlite3.connect(DB, uri=True, isolation_level=None)


def foreign_keys(conn):
    return conn.execute("PRAGMA foreign_keys").fetchone()[0]


def rejected(conn, child_id):
    try:
        conn.execute("INSERT INTO child VALUES (?, ?)", (child_id, 999))
    except sqlite3.IntegrityError as exc:
        if exc.sqlite_errorcode != sqlite3.SQLITE_CONSTRAINT_FOREIGNKEY:
            raise
        return exc.sqlite_errorname
    raise AssertionError("invalid reference was accepted")


try:
    # 显式设置,避免依赖 SQLite 编译时的默认值。
    a.execute("PRAGMA foreign_keys = OFF")
    b.execute("PRAGMA foreign_keys = ON")
    a.execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
    a.execute("CREATE TABLE child (id INTEGER PRIMARY KEY, "
              "parent_id INTEGER NOT NULL REFERENCES parent(id))")
    print("A/B foreign_keys:", foreign_keys(a), foreign_keys(b))
    assert foreign_keys(a) == 0 and foreign_keys(b) == 1

    a.execute("INSERT INTO child VALUES (1, 999)")
    print("B sees:", b.execute("SELECT * FROM child").fetchall())
    print("B rejects invalid row:", rejected(b, 2))

    a.execute("BEGIN")
    a.execute("PRAGMA foreign_keys = ON")  # 事务中设置不生效
    print("A inside transaction:", foreign_keys(a))
    assert foreign_keys(a) == 0
    a.execute("ROLLBACK")
    a.execute("PRAGMA foreign_keys = ON")
    print("A after transaction:", foreign_keys(a))
    assert foreign_keys(a) == 1
    print("A rejects invalid row:", rejected(a, 2))

    a.execute("INSERT INTO parent VALUES (1)")
    a.execute("INSERT INTO child VALUES (3, 1)")
    print("child rows:", a.execute("SELECT * FROM child ORDER BY id").fetchall())
    print("integrity_check:", a.execute("PRAGMA integrity_check").fetchone()[0])
    print("foreign_key_check:", a.execute("PRAGMA foreign_key_check").fetchall())
finally:
    b.close()
    a.close()

本机输出:

A/B foreign_keys: 0 1
B sees: [(1, 999)]
B rejects invalid row: SQLITE_CONSTRAINT_FOREIGNKEY
A inside transaction: 0
A after transaction: 1
A rejects invalid row: SQLITE_CONSTRAINT_FOREIGNKEY
child rows: [(1, 999), (3, 1)]
integrity_check: ok
foreign_key_check: [('child', 1, 'parent', 0)]

两个连接通过同一个命名内存库共享数据。示例用 isolation_level=None 关闭 Python 的隐式事务,并显式开启一个事务。A 关闭外键检查后写入 (1, 999);B 能看到这条记录,却拒绝了自己尝试写入的无效引用。事务中的 A 无法开启检查;回滚事务后再开启,才会拒绝相同的无效写入。(3, 1) 引用已存在的父记录,所以可以写入。

开启检查不会自动修复历史数据。输出末尾的 foreign_key_check 表示 child 表的 rowid 为 1 的记录引用了 parent,违反外键编号 0。此例中 id 是 INTEGER PRIMARY KEY,因此也是该行的 rowid。虽然 integrity_check 返回 ok,它不检查外键;排查旧数据时要单独使用 foreign_key_check。检查结果应结合业务规则修复,不应直接批量删除。

用于实际项目时,在每个新连接建立后、开启事务前设置并读取 PRAGMA foreign_keys,确认值为 1;连接池也要在每个物理连接初始化时处理。不要依赖默认值,SQLite 的编译配置可能影响它。对于必须有父记录的字段,还要声明 NOT NULL:外键列允许 NULL 时,空值不会要求匹配父记录。

参考资料:SQLite 官方文档《Foreign Key Support》《PRAGMA foreign_key_check》;Python 官方文档《sqlite3 — DB-API 2.0 interface for SQLite databases》。

本文借助 AI 辅助整理,示例经运行核验。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。