【Django】get_or_createしているのに一意制約違反が発生する

はじめに

こんにちは、SHOJIです。

Djangoで発生したエラー対応の備忘録です。

公式ドキュメントを読む大切さをあらためて教えてくれたので戒めとして記事にします。

get_or_createしても同時に複数のリクエストを受けると一意制約違反が発生

複数テーブルのデータを一括更新する処理があり、最初に管理テーブルから一意のIDを採番して、採番したIDをキーとして各テーブルのデータを更新するという流れにしていました。更新はデータの状態によって、既存データの更新新規データの追加の2パターンあるため、get_or_createを使って既存データの有無に関わらず一意のIDを取得して後続処理に進むようにしていました。

ユニットテストでは正しく動いているように見えましたが、同時アクセスのテストで一意制約違反(UNIQUE constraint failed)が発生しました。

一意制約違反ということは、get_or_createのcreateのタイミングできっとこういうことが起きているはずです。

  1. [Aプロセス] ID:Xが存在するか確認

  2. [Bプロセス] ID:Xが存在するか確認

  3. [Aプロセス] ID:Xが存在しないため追加

  4. [Bプロセス] ID:Xが存在しないため追加 ← すでにAプロセスがID:Xを追加しているので一意制約違反

get_or_createの意味がまるでない……。

これは何かおかしいぞと思って調べたところ、StackOverFlowにこのような投稿がありました。

stackoverflow.com

ベストアンサーを読むに、キー項目以外を指定すると質問の例のように重複した値であっても機能しないとのこと。 言われるがままにキー項目以外の指定をせずに再度テストを実行してみると、たしかに一意制約違反は出なくなりました。


エラーは出なくなりましたが、重複した値でも動作しない仕様というのがしっくり来ず、公式ドキュメントを見に行くと思いっきりWarningとして書かれていました笑

docs.djangoproject.com

そもそもの仕様として良い形であるかは議論の余地がありそうですが(なぜそうしているのか分からないので否定もできませんが)、get_or_createの仕様としては、キー項目のみを指定しない場合は複数行の追加が発生しうるとのことです。

おわりに

「はじめに」で書いたとおり、名前から効果を予測しやすいメソッドでもあっても、ちゃんと公式ドキュメントを読んで使わないといけないですね。


……と言いたいところですが、全てのドキュメントを一から十まで読む時間があれば苦労はないわけで。 チームで機能ごとのスペシャリストを立てるとか(DjangoはAさんが詳しく、Django REST FrameworkはBさんが詳しいといったような)、テストをしっかり行うことでカバーするとか、「ドキュメントを読め!」以外の対策を考えておかないとどこかで破綻するだろうなと思います。