この記事はDjangoもくもく会: 2回目 で書いた記事です。
Django4.1からbulkのupsert的な処理ができるようになっていたという記事を先週書いた。
そして最後に以下のコメントを書いた。
bulk_createに引数を追加して本機能を実装するのではなく、bulk_upsertやbulk_update_or_createがあっても良いのでは?と言う感想を持ったので、なぜbulk_createに作成されたのか、経緯をちょっと調べてみようと思った。
すると某先輩からの後押しもあり、続けてブログを書くことにした。
続編期待
— かしゅー (@kashew_nuts) 2024年6月5日
自分が調べた上での結論
想像になってしまうが、Issue・コードを見た上での自分の考えの結論。
何か詳しい方がいたら教えてください。
- 最初から
bulk_create
に追加する前提でチケットが切られていた - SQLの構文的に
INSERT INTO~
から続くので、INSERT文を発行する側に追加する方が自然な感じがした - (PostgreSQLだけど)
ON CONFLICT DO NOTHING
を追加するignore_conflicts_suffix_sql
が当時存在していたので、なおさら追加した方が良さそうだった
起点となるチケット・プルリクエスト
Support updating conflicts with QuerySet.bulk_create(). で起票されていた。
ここでは以下のように起票されており、bulk_create
に引数を追加する前提で書かれていた。
It would be useful having a parameter in bulk_create(), like bulk_create(objs, upsert=True) or bulk_create(objs, update_conflicts=True), that lets you update existing rows when there's a conflict, much like what you can already do for a single entity with update_or_create().
bulk_create
に対して主にupsert機能を実装したチケットはFixed #31685 -- Added support for updating conflicts to QuerySet.bulk_create(). #13065
のようだった。
実際にコードを見てみる
まず、bulk_create
に引数が追加されている。
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
だったのが、以下のようになっていた。
def bulk_create( self, objs, batch_size=None, ignore_conflicts=False, update_conflicts=False, update_fields=None, unique_fields=None, ):
その後、on_conflict
という変数が増えていた。
これは_check_bulk_create_options
というメソッドの返り値を保持してコンフリクトした時の設定を持つようだった。
on_conflict = self._check_bulk_create_options( ignore_conflicts, update_conflicts, update_fields, unique_fields, )
_check_bulk_create_options
ではバリデーションチェックをした後、Enum
を利用したOnConflict.UPDATE
または None
を返却していた。
その後はトランザクションを張ってSQLを実際に実行しているようだった。
https://github.com/django/django/blob/stable/4.1.x/django/db/models/query.py#L796
他のポイント
元々、ignore_conflicts_suffix_sql
というメソッドが存在していたようだった。
django/db/backends/base/operations.py
変更前
def ignore_conflicts_suffix_sql(self, ignore_conflicts=None): return ''
変更後
def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields): return ''
ignore_conflicts_suffix_sql
のメソッド名がon_conflict_suffix_sql
となっており、もう少し振る舞いが「広がった」ことがわかる。
こちらは親クラス側なので、特にロジックは書かれていない。
django/db/backends/postgresql/operations.py
そして実際にPostgreSQLのSQLを組み立てるoperations.py
側のコードが変わっていた。
変更前
def ignore_conflicts_suffix_sql(self, ignore_conflicts=None): return 'ON CONFLICT DO NOTHING' if ignore_conflicts else super().ignore_conflicts_suffix_sql(ignore_conflicts)
変更後
def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields): if on_conflict == OnConflict.IGNORE: return 'ON CONFLICT DO NOTHING' if on_conflict == OnConflict.UPDATE: return 'ON CONFLICT(%s) DO UPDATE SET %s' % ( ', '.join(map(self.quote_name, unique_fields)), ', '.join([ f'{field} = EXCLUDED.{field}' for field in map(self.quote_name, update_fields) ]), ) return super().on_conflict_suffix_sql( fields, on_conflict, update_fields, unique_fields, )
上記のコードを見ると、「わざわざupsertを作るより、この既存ロジックに追加した方が自然かな」という気持ちになった。
また、SQL的にも INSERT INTO~
の後にコンフリクトした場合の記述を行うので、UPDATE側には書かないだろうな、という風にも思った。