update_or_create()でupsertのような挙動を実現させる

Djangoを利用してupsertのような挙動を実現するためには、update_or_create() を利用すれば良いようだった。

upsertとは

SQLにおいて、既にデータが登録されていたらUPDATEを行い、データがなければINSERTを行うという処理の事を指している。軽く調べたところ、INSERTUPDATE のように UPSERT というコマンドがあるわけではなく、例えばPostgreSQLではMERGEコマンドを利用して同様の動作を実現しているようだった。

update_or_create()について

公式リファレンス のupdate_or_create()によると、例えばtry~except model名.DoesNotExistを利用して、save or update を呼び出すようなコードをupdate_or_create()を利用すれば記述できると書かれてある。

実際に書いてみた一例

以下のリポジトリのコードを利用して記述。 https://github.com/nibuno/djangoTaskApp

Djangoのバージョンは4.2.9で試した。

from datetime import date

user = User.objects.get(username="nibutan")

# 1回目のINSERT処理
task, created = Task.objects.update_or_create(
    title="ログを出力する",
    created_user=user,
    defaults={
        "content": "どんな操作がされたのかログに残して分かるようにする。",
        "status": 1,
        "order": 10,
        "limit_date": date(2024, 6, 1),
        "updated_user": user
    }
)

# 2回目のUPDATE処理
task, created = Task.objects.update_or_create(
    title="ログを出力する",
    created_user=user,
    defaults={
        "content": "どんな操作がされたのかログに残して分かるようにする。django-structlogを使ってみるのも良さそう。",
        "status": 1,
        "order": 10,
        "limit_date": date(2024, 6, 1),
        "updated_user": user
    }
)

上記のように書くことで、いわゆるupsertの処理を実現することができた。

また、例によって発行されるSQLを確認したところ、以下のようにシンプルなUPDATE文の発行(UPSERT処理を書いているわけではない)だった。これは、DBによってupsertを実現するための構文が異なるのでDjango側で吸収しているのだと想像している。

# 初回のINSERT時
(0.000) BEGIN; args=None; alias=default
(0.001) SELECT "task"."id", "task"."title", "task"."content", "task"."status", "task"."order", "task"."created_user_id", "task"."limit_date", "task"."updated_user_id", "task"."created_at", "task"."updated_at" FROM "task" WHERE ("task"."created_user_id" = 1 AND "task"."title" = 'ログを出力する' 21; args=(1, 'ログを出力する'); alias=default
(0.000) SAVEPOINT "s281473828298784_x2"; args=None; alias=default
(0.009) INSERT INTO "task" ("title", "content", "status", "order", "created_user_id", "limit_date", "updated_user_id", "created_at", "updated_at") VALUES ('ログを出力する', 'どんな操作がされたのかログに残して分かるようにする', 1, 10, 1, '2024-06-01', 1, '2024-06-01 07:04:03.835112', '2024-06-0ask"."id"; args=('ログを出力する', 'どんな操作がされたのかログに残して分かるようにする', 1, 10, 1, '2024-06-01', 1, '2024-06-01 07:04:03.835112', 'ias=default
(0.000) RELEASE SAVEPOINT "s281473828298784_x2"; args=None; alias=default
(0.005) COMMIT; args=None; alias=default

# 2回目のUPDATE時
(0.000) BEGIN; args=None; alias=default
(0.001) SELECT "task"."id", "task"."title", "task"."content", "task"."status", "task"."order", "task"."created_user_id", "task"."limit_date", "task"."updated_user_id", "task"."created_at", "task"."updated_at" FROM "task" WHERE ("task"."created_user_id" = 1 AND "task"."title" = 'ログを出力する' 21; args=(1, 'ログを出力する'); alias=default
(0.002) UPDATE "task" SET "content" = 'どんな操作がされたのかログに残して分かるようにする。django-structlogを使ってみるのも良さそう。', "status" = 6-01', "updated_user_id" = 1, "created_at" = '2024-06-01 07:04:03.835112', "updated_at" = '2024-06-01 07:05:18.849078' WHERE "task"."id" = 42; args=('どんな操作がされたのかログに残して分かるようにする。django-structlogを使ってみるのも良さそう。', 1, 10, '2024-06-01', 1, '2024-06-01 07:04:03.83); alias=default
(0.003) COMMIT; args=None; alias=default