前回書いた記事のupdate_or_create()でupsertのような挙動を実現させるは1行ずつ処理をするものだった。
しかし、レコードの登録件数が多い時など、DBに対する負荷を考慮した場合、いわゆるbulk
処理で一括挿入・更新することが求められるケースがある。
そうしたケースにもbulk_create
にオプションが追加され、Django4.1から対応できるようになっていたようだった。
https://docs.djangoproject.com/ja/5.0/releases/4.1/#models には以下のように書かれていた。
QuerySet.bulk_create() now supports updating fields when a row insertion fails uniqueness constraints. This is supported on MariaDB, MySQL, PostgreSQL, and SQLite 3.24+.
bulk_createとは
自分の中では、レコードを一括挿入するSQLを作成してくれるQuerySet APIと理解している。
リファレンスには以下のように書かれている。
This method inserts the provided list of objects into the database in an efficient manner (generally only 1 query, no matter how many objects there are), and returns created objects as a list, in the same order as provided:
https://docs.djangoproject.com/ja/4.2/ref/models/querysets/#bulk-create より引用。
引数を自分なりにまとめたものが以下。
引数 | 内容 | デフォルト値 |
---|---|---|
objs | 対象のModel群 | なし |
batch_size | 1回のクエリで作成されるオブジェクトの数。※SQLiteはデフォルト値999なのでそれ以外のDBが対象。 | None |
ignore_conflicts | Trueにした場合、重複して失敗した行の挿入を無視する。 | False |
update_conflicts | Trueにした場合、重複して失敗した場合失敗した行を更新する。 | False |
update_fields | update_conflictsをTrueにした場合、更新する対象のフィールド。 | None |
unique_fields | PostgreSQLとSQLiteではupdate_conflicts指定時に競合する可能性のあるリストを指定する。 | None |
update_conflicts
, update_fields
, unique_fields
がDjango4.1から追加された引数だった。
また、ignore_conflicts
とupdate_conflicts
を両方TrueにするとValueError
が発生する。
実際に動かしてみた
部署名と課名のフィールドを持つModelを定義する。
class Department(models.Model): """所属部署""" name = models.CharField("名前", max_length=100, unique=False) section_name = models.CharField("課名", max_length=100, unique=True)
django-extensionsのshell_plusを利用して以下のコードを実行した。
1回目のデータ投入
ここでは、すべてINSERTされる想定。
>>> departments = [ ... Department(id="1", name="営業部", section_name="営業課"), ... Department(id="2", name="仕入部", section_name="仕入課"), ... Department(id="3", name="物流部", section_name="物流課"), ... ] >>> >>> Department.objects.bulk_create( ... objs=departments, ... update_conflicts=True, ... update_fields=["name", "section_name"], ... unique_fields=["id"], ... ) (0.000) BEGIN; args=None; alias=default (0.006) INSERT INTO "company_department" ("id", "name", "section_name") VALUES (1, '営業部', '営業課'), (2, '仕入部', '仕入課'), (3, '物流部', '物流課') ON CONFLICT("id") DO UPDATE SET "name" = EXCLUDED."name", "section_name" = EXCLUDED."section_name"; args=(1, '営業部', '営業課', 2, '仕入部', '仕入課', 3, '物流部', '物流課'); alias=default (0.008) COMMIT; args=None; alias=default [<Department: 営業部>, <Department: 仕入部>, <Department: 物流部>]
SQLがINSERT INTO~ ON CONFLICT DO UPDATE SET
と出力されており、これでUPSERT的処理を実現していることが理解できた。
また、レコードがINSERTされていることも確認できた。
sqlite> select * from company_department; 1|営業部|営業課 2|仕入部|仕入課 3|物流部|物流課
2回目のデータ投入。
ここでは、INSERT・UPDATE(および何もされない)想定で記述した。
>>> departments = [ ... Department(id="1", name="営業部", section_name="営業1課"), # 営業1課にUPDATE ... Department(id="2", name="仕入部", section_name="仕入1課"), # 仕入1課にUPDATE ... Department(id="3", name="物流部", section_name="物流課"), # ここはそのまま ... Department(id="4", name="営業部", section_name="営業2課"), # 新規INSERT ... Department(id="5", name="商品企画部", section_name="商品企画課"), # 新規INSERT ... ] >>> >>> Department.objects.bulk_create( ... objs=departments, ... update_conflicts=True, ... update_fields=["name", "section_name"], ... unique_fields=["id"], ... ) (0.000) BEGIN; args=None; alias=default (0.007) INSERT INTO "company_department" ("id", "name", "section_name") VALUES (1, '営業部', '営業1課'), (2, '仕入部', '仕入1課'), (3, '物流部', '物流課'), (4, '営業部', '営業2課'), (5, '商品企画部', '商品企画課') ON CONFLICT("id") DO UPDATE SET "name" = EXCLUDED."name", "section_name" = EXCLUDED."section_name"; args=(1, '営業部', '営業1課', 2, '仕入部', '仕入1課', 3, '物流部', '物流課', 4, '営業部', '営業2課', 5, '商品企画部', '商品企画課'); alias=default (0.009) COMMIT; args=None; alias=default [<Department: 営業部>, <Department: 仕入部>, <Department: 物流部>, <Department: 営業部>, <Department: 商品企画部>]
レコードがUPDATE, INSERTされていることも確認できた。
sqlite> select * from company_department; 1|営業部|営業1課 2|仕入部|仕入1課 3|物流部|物流課 4|営業部|営業2課 5|商品企画部|商品企画課
雑感
bulk_create
に引数を追加して本機能を実装するのではなく、bulk_upsert
やbulk_update_or_create
があっても良いのでは?と言う感想を持ったので、なぜbulk_create
に作成されたのか、経緯をちょっと調べてみようと思った。