「重複キーが一意性制 auth_permission_pkey に違反しています」それは突然やってきた

この投稿は約3分で読めます

Django


「うし、レビュー終わった!ブランチ切り替えて実装の続きしよう!」と、いつものようにブランチを切り替えたときでした。
それは突然やってきたのです。

File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
return self.cursor.execute(sql, params)
django.db.utils.IntegrityError: 重複キーが一意性制約"auth_permission_pkey"に違反しています
DETAIL:  キー (id)=(177) はすでに存在します。

Django のマイグレーションで上記のような IntegrityError が発生し、ローカル環境が立ち上がらなくなってしまいました。
showmigrations でマイグレーションの適用状況を確認すると、マイグレーションはすべて適用されていました。

思い出すんだ、数時間前の自分の行動を・・・

遡ること数時間前、こんなことをしていました。

  • モデルのコードレビューで、マイグレーションやロールバックを何度か行った
  • 途中、ロールバックし忘れたままブランチを切り替えてマイグレーションを行った

おそらく、上記の作業中に IntegrityError を引き起こすような何かがあったのだと思います。

そして、当該モデルの実装をしていたメンバーの環境でも同じエラーが出ていたらしく、下記コマンドで修正したとのことでした。
が、もう少し自分でも調べてみることにしました。

SELECT setval('auth_permission_id_seq', (SELECT MAX(id) FROM auth_permission));

ローカル環境が落ちればメンタルも落ちる

落ちまくりのローカル環境と精神衛生を回復すべく、同じような問題に遭遇している人がいないか検索したところ、以下の投稿を見つけることができました。
django.db.utils.IntegrityError: duplicate key value violates unique constraint “auth_permission_pkey” (stackoverflow)

上記の Answer で、以下のブログが参照されていました。
How to reset the primary key sequence in PostgreSQL with Django (Calazan.com)

ブログの内容をざっくりかい摘むと、

  • 新しいモデルを追加すると、auth_permission テーブルにレコードが追加される
  • マイグレーションすると、上記テーブルの pkey のシーケンスが自動的にインクリメントされる
  • そのため、既存レコードの pkey とコンフリクトしない ID になるまでマイグレーションを何度か実行すればエラーは出なくなる
  • Django には、シーケンスをリセットするための sqlsequencereset コマンドが用意されている

というような内容が書かれていました。
ふむふむ、なるほど。さっそく手元でも試してみました。

落ちたらあとは登るだけ ()

まずは、migrate を実行してみると、(id)=(177) だった部分が (id)=(178) にインクリメントされました。

django.db.utils.IntegrityError: 重複キーが一意性制約"auth_permission_pkey"に違反しています
DETAIL:  キー (id)=(178) はすでに存在します。

上記ブログによると、このまま何度か migrate を実行すれば、いずれは重複しない ID に到達してマイグレートできるようになるとのこと。
ですが、このまま migrate で解決しちゃうのはもったいないので (?)、sqlsequencereset コマンドも試してみました。

$ python manage.py sqlsequencereset auth
...
BEGIN;
SELECT setval(pg_get_serial_sequence('"auth_permission"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "auth_permission";
SELECT setval(pg_get_serial_sequence('"auth_group_permissions"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "auth_group_permissions";
SELECT setval(pg_get_serial_sequence('"auth_group"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "auth_group";
COMMIT;

これは便利、シーケンスリセット用の SQL を手に入れることができました!
この SQL を dbshell で実行したところ、無事にシーケンスがリセットされ、ローカル環境も精神衛生も回復しました!

まとめ

  • モデル作成、マイグレート、ロールバック、ブランチ切り替えなどをあれこれ試していると、auth_permissionpkey のシーケンスが、既存のものと同期が取れなくなることがある
  • 同期が取れないと、「重複キーが一意性制 auth_permission_pkey に違反しています」がやって来る
  • SELECT setval('auth_permission_id_seq', (SELECT MAX(id) FROM auth_permission)); を実行しても直せるが、sqlsequencereset を使うのが公式な解決手段なもよう

以上、現場からでした。

参考