春江暮客

春江暮客的个人学习分享网站

Python SQLite 外键不生效:连接开关、整批回滚与历史孤儿记录检查

2026-10-31 技术
Python SQLite 外键不生效:连接开关、整批回滚与历史孤儿记录检查

SQLite 表里已经写了 REFERENCES,批处理却仍能插入不存在的任务 ID。问题可能不在 SQL 拼写,而在当前连接没有启用外键检查。本文用一个不接触真实数据库的内存实验,对比开关前后的同一批输入,再验证整批回滚和历史孤儿记录检查,适合初级到中级读者。

1. 方法:建约束、开连接检查、再做历史审计

外键约束描述表之间的关系;运行时还要确认连接是否执行它。不要假设服务器上的 SQLite 构建默认开启检查:连接后主动设置 PRAGMA foreign_keys = ON,紧接着查询返回值,要求为 1。连接池或第二个进程建立的连接也需要这一步。SQLite 官方启用说明明确说明了连接级开关及事务内不能切换的限制。

这里的父表 jobs 存任务,子表 results 存结果。results.job_id 必须指向已有任务;同时加 NOT NULL,避免空值绕过这一必填关系。示例不设置级联删除:有子记录时,删除父任务应失败。

这不是备份替代品,也不是对现有库的自动修复工具。操作真实数据前,先做 SQLite 备份和恢复验证;本页所有数据库都只存在于内存中。

2. 准备:只使用 Python 标准库

需要 Python 3.11 或更新版本和可用的 sqlite3 模块,无需 pip 包或 SQLite 命令行。示例在 Linux、Python 3.11.4 下测试。按顺序执行,后续命令留在同一个新目录:

python3 --version
python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'
mkdir sqlite-fk-demo
cd sqlite-fk-demo

将下一节保存为 fk_demo.py,也可下载完整演示脚本。它只接受 --enforce on 或 --enforce off,默认 on;off 仅用于故障对照。

3. 实现:在 BEGIN 之前设置并核对开关

"""Compare foreign-key enforcement in a disposable in-memory database."""
import argparse
import json
import sqlite3
import sys


def open_db(enforce=True):
    # No implicit BEGIN: all write transactions below are explicit.
    con = sqlite3.connect(":memory:", isolation_level=None)
    try:
        con.execute("PRAGMA foreign_keys = " + ("ON" if enforce else "OFF"))
        expected = (1 if enforce else 0,)
        if con.execute("PRAGMA foreign_keys").fetchone() != expected:
            raise RuntimeError("foreign-key setting was not applied")
        con.execute("CREATE TABLE jobs (id INTEGER PRIMARY KEY)")
        con.execute("""CREATE TABLE results (
            id INTEGER PRIMARY KEY,
            job_id INTEGER NOT NULL REFERENCES jobs(id)
        )""")
        con.execute("CREATE INDEX results_job_id ON results(job_id)")
        con.execute("INSERT INTO jobs VALUES (1)")
        return con
    except Exception:
        con.close()
        raise


def run_demo(enforce=True):
    con = open_db(enforce)
    error = None
    try:
        try:
            con.execute("BEGIN")
            con.execute("INSERT INTO results VALUES (?, ?)", (1, 1))
            con.execute("INSERT INTO results VALUES (?, ?)", (2, 999))
            con.execute("COMMIT")
        except sqlite3.IntegrityError as exc:
            con.execute("ROLLBACK")
            error = str(exc)
        report = {
            "accepted": error is None,
            "foreign_keys": con.execute("PRAGMA foreign_keys").fetchone()[0],
            "result_rows": con.execute("SELECT COUNT(*) FROM results").fetchone()[0],
            "violations": con.execute("PRAGMA foreign_key_check").fetchall(),
        }
        return report, error
    finally:
        con.close()


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("--enforce", choices=("on", "off"), default="on")
    args = parser.parse_args()
    report, error = run_demo(args.enforce == "on")
    print(json.dumps(report, sort_keys=True))
    if error:
        print("ERROR: " + error, file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

isolation_level=None 关闭 Python 的隐式事务开启行为;写入批次明确执行 BEGIN、COMMIT 或 ROLLBACK。参见 Python sqlite3 事务控制。这里没有使用 Python 3.12 才增加的 autocommit 参数;不要混入一个总是保持事务打开的连接策略。

先建立任务 1,再尝试同一批写入 (1, 1) 和 (2, 999)。第二条记录引用不存在的任务:开启检查时,异常处理显式回滚整个批次,因此第一条有效结果也不会留下。单条语句失败不等于前面所有写入自动撤销。

results_job_id 索引方便按父键查找子记录;它不是启用外键的开关。SQL 值通过 ? 绑定,不用字符串拼接;PRAGMA 中拼接的仅是代码内固定的 ON、OFF。

4. 验证:同一批输入,比较行数与退出码

python3 fk_demo.py --help
python3 fk_demo.py --enforce off
if python3 fk_demo.py --enforce on; then
  printf 'UNEXPECTED: invalid batch accepted\n' >&2
  exit 1
else
  rc=$?
  printf 'protected exit=%s\n' "$rc"
  test "$rc" -eq 1
fi

关闭检查时,标准输出如下,退出码为 0,标准错误为空:

{"accepted": true, "foreign_keys": 0, "result_rows": 2, "violations": [["results", 2, "jobs", 0]]}

开启检查时,标准输出如下;标准错误是 ERROR: FOREIGN KEY constraint failed,脚本退出 1,shell 随后打印 protected exit=1:

{"accepted": false, "foreign_keys": 1, "result_rows": 0, "violations": []}

result_rows: 0 是整批回滚的结果;accepted: false 仍是失败报告,不能因为 stdout 有 JSON 就当作成功。自动化必须同时检查退出码。错误的 --enforce 值由 argparse 返回 2,帮助返回 0。

可下载回归测试,和脚本放在同一目录后运行:

python3 test_fk_demo.py

测试会检查退出码、结果行数、stderr、历史违规、事务内开关无效以及拒绝删除被引用父行。它不连接生产数据库,也不会生成数据库文件。

5. 检查历史违规:开启不等于修复

下面另存为 legacy_check.py,在演示目录执行 python3 legacy_check.py:

from fk_demo import open_db

con = open_db(False)  # Reproduce a legacy writer only; not a production setting.
try:
    con.execute("INSERT INTO results VALUES (7, 999)")
    con.execute("PRAGMA foreign_keys = ON")
    print("enabled:", con.execute("PRAGMA foreign_keys").fetchone()[0])
    print("violations:", con.execute("PRAGMA foreign_key_check").fetchall())
finally:
    con.close()

输出:

enabled: 1
violations: [('results', 7, 'jobs', 0)]

PRAGMA 官方说明定义了每行的四项含义:子表、问题行的 rowid、父表、外键编号。开启检查不会删除或修复已经存在的孤儿记录;这里的 7 是普通 rowid 表中出问题的行。

迁移旧库时,先在稳定备份或停止写入的副本上运行 PRAGMA foreign_key_check。非空结果应阻止验收,然后按业务依据补父记录、修正引用或隔离坏数据,不要直接批量删除。没有声明外键的表不会因这条命令获得关系校验;空结果也不是完整的业务验证或磁盘完整性保证。

6. 排障:先区分约束失败与锁冲突

现象 直接处理
设置 ON 后仍查询到 0 检查 con.in_transaction;先明确提交或回滚当前业务事务,再在事务外设置。不要为了切换开关而盲目提交未验收数据。
PRAGMA foreign_keys 返回空行 检查 SQLite 是否具备外键支持;保留连接预检,让任务失败,而不是带着未知状态继续。
FOREIGN KEY constraint failed 核对父 ID 是否已存在、导入顺序是否先父后子,并回滚失败批次;不要用 OFF 作为修复。
foreign key mismatch 检查父键是否是主键或满足外键要求的唯一键,并核对列数和排序规则;改表前先备份。
重连之后又能写入坏记录 将设置与查询集中在连接工厂;每个写入入口都必须调用。
database is locked 这是写入竞争,不是缺少父键;按短事务与有限等待排障处理。

如果批处理需要断点续跑,把结果和进度的数据库修改放进同一事务。外键只能约束数据库内的关系,不保证外部文件、HTTP 请求或整个工作流恰好执行一次。

总结

声明外键之后,还要为每个连接在事务外启用并核对检查状态。用同一批合法与非法输入验证拒绝和整批回滚,再单独审计历史违规。保留备份和业务修复依据,不把关闭检查当作修复手段。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它