@kyanny's blog

私は天才ではありません。ただ、人より長く一つの事と付き合っていただけです - アインシュタイン

Django: save() に必ず INSERT させるためには force_insert=True を使う

ナチュラルキー + 複合主キーを使っているテーブル(のモデル)に対してループのなかで save() したら一行しか INSERT されなくてハマった。

  • Django は primary key の attribute 値が None ではないオブジェクトに対して save() を呼ぶと、まず UPDATE を実行する。 UPDATE が一行も変更しなかった場合は続けて INSERT を実行する。
  • サロゲートキーを使っている場合、 primary key の attribute 値が重複していたら DB 制約により INSERT できないので、この種の問題は起きない。
  • テーブルに複合主キーが設定されている場合も、 Django Model の定義上は複合主キーを宣言できないので、 Django アプリを動かすためには一つの attribute に対して primary_key=True を宣言することになる。 ref
  • この状態で、親子関係の子にあたるモデルのインスタンスを複数作成して同じ親を参照させて save() すると、一行だけ INSERT されて残りの save() は作成した行を UPDATE する、という挙動が発生する。

検証用に作ったサンプルプロジェクト

github.com

要は、ここの違いが重要。

In [8]: for i in range(3):
   ...:     c = Choice(question=q, choice_text='hi', votes=i)
   ...:     c.save()
   ...:
   ...:
(0.000) UPDATE "polls_choice" SET "choice_text" = 'hi', "votes" = 0 WHERE "polls_choice"."question_id" = 3; args=('hi', 0, 3)
(0.001) INSERT INTO "polls_choice" ("question_id", "choice_text", "votes") SELECT 3, 'hi', 0; args=(3, 'hi', 0)
(0.001) UPDATE "polls_choice" SET "choice_text" = 'hi', "votes" = 1 WHERE "polls_choice"."question_id" = 3; args=('hi', 1, 3)
(0.002) UPDATE "polls_choice" SET "choice_text" = 'hi', "votes" = 2 WHERE "polls_choice"."question_id" = 3; args=('hi', 2, 3)
In [6]: for i in range(3):
   ...:     c = Choice(question=q, choice_text='hi', votes=i)
   ...:     c.save(force_insert=True)
   ...:
(0.003) INSERT INTO "polls_choice" ("question_id", "choice_text", "votes") SELECT 7, 'hi', 0; args=(7, 'hi', 0)
(0.002) INSERT INTO "polls_choice" ("question_id", "choice_text", "votes") SELECT 7, 'hi', 1; args=(7, 'hi', 1)
(0.001) INSERT INTO "polls_choice" ("question_id", "choice_text", "votes") SELECT 7, 'hi', 2; args=(7, 'hi', 2)

当然、 save() ではなく create() を使うことでも解決できる。というか、 create() は↑のショートハンドに過ぎないらしい。

QuerySet API reference | Django documentation | Django

結論: ナチュラルキーなんて使うもんじゃない。この手のフルスタック Web Application Framework とか O/R Mapper はサロゲートキーを暗黙に期待するものが多いだろうから、大人しく慣習に合わせておいた方が無難。(まぁ、 O/R Mapper に合わせてテーブル設計するのは本末転倒ではあるが)