SQLite で外部キー制約が効かない(存在しない ID を INSERT できてしまう)
SQLite の外部キー制約は既定で無効で、接続ごとに PRAGMA foreign_keys = ON を実行して初めて効く。
トランザクションの途中で実行すると黙って無視されるので、接続を開いた直後に実行し、PRAGMA foreign_keys で 1 が返ることを確かめる。
公開:
要約
REFERENCES を付けて外部キーを定義したのに、存在しない親の ID を持つ行がそのまま入る。ON DELETE CASCADE を書いても、親を消したときに子が消えない。
CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE posts(
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title TEXT
);
INSERT INTO posts(user_id, title) VALUES (999, 'orphan'); -- エラーにならない公式ドキュメントのとおり、SQLite の外部キー制約は後方互換のために既定で無効になっている。
有効にするには、接続ごとに次を実行する。
PRAGMA foreign_keys = ON;有効な接続で同じ INSERT を実行すると、Python では sqlite3.IntegrityError: FOREIGN KEY constraint failed になる。
よくある原因
- PRAGMA を実行していない: テーブル定義に外部キーを書いただけでは、制約は検査されない。
- 接続ごとに実行していない: 設定はデータベースファイルではなく接続に付く。
一度実行しても、別の接続や再接続した接続では OFF に戻る。 - トランザクションの途中で実行した: 未コミットの書き込みがある状態で実行するとエラーは出ず、単に効かない。
Python のsqlite3はINSERTなどの前に暗黙にトランザクションを開くので、書き込みの後に実行すると無視される。 autocommit=Falseで接続している: Python 3.12 で追加されたautocommit=Falseでは、sqlite3が常にトランザクションを開いた状態を保つ。
そのため、この接続で実行したPRAGMA foreign_keys = ONは効かない。- 違反した行がすでにある: 有効にしても、それまでに入った親の無い行は自動では検出されない。
解決策
1. 接続を開いた直後に有効にする
import sqlite3
def connect(path):
con = sqlite3.connect(path)
con.execute("PRAGMA foreign_keys = ON")
return conアプリの中で接続を作る処理をこの関数に集め、どの経路でも必ず通るようにする。
公式ドキュメントも、既定値に頼らずアプリケーション側で明示的に設定するよう勧めている。
2. 効いているかを確かめる
PRAGMA foreign_keys;1 が返れば有効、0 なら無効だ。
何も返らない場合は、その SQLite が外部キーに対応しない設定でビルドされているか、3.6.19 より古い。
3. autocommit=False のときは切り替える前に設定する
con = sqlite3.connect("app.db", autocommit=True)
con.execute("PRAGMA foreign_keys = ON")
con.autocommit = Falseautocommit を False に切り替えた時点でトランザクションが開くので、設定はその前に済ませる。
4. 既存の違反を洗い出す
PRAGMA foreign_key_check;違反している行ごとに、子テーブル名・その行の rowid・参照先テーブル名・外部キーの番号が返る。
該当する行の親を補うか、行を削除してから運用を始める。