はじめに
DjangoでDBを一括更新する
の記事ではDjangoのORMでbulk updateを実行した時の以下のような
CASE WHEN構文とWHERE ... IN構文
を使ったクエリを発行することが述べられています
UPDATE `customer`
SET `rate` =
CASE
WHEN (`customer`.`id` = 1) THEN 5
WHEN (`customer`.`id` = 2) THEN 4
WHEN (`customer`.`id` = 3) THEN 3
ELSE NULL
END
WHERE `customer`.`id` IN (1, 2, 3);
果たしてどんな時も基本的に似たようなクエリが発行されるのかを
django.db.models.query.QuerySet.bulk_update
のソースを軽く読んでみて確認します。
環境
Django==2.2.13
MySQL==5.7
実際に読んでみる
以下がbulk_updateの中核となるロジックです
def bulk_update(self, objs, fields, batch_size=None):
```
中略
```
updates = []
for batch_objs in batches:
update_kwargs = {}
for field in fields:
when_statements = []
for obj in batch_objs:
attr = getattr(obj, field.attname)
if not isinstance(attr, Expression):
attr = Value(attr, output_field=field)
when_statements.append(When(pk=obj.pk, then=attr))
case_statement = Case(*when_statements, output_field=field)
if requires_casting:
case_statement = Cast(case_statement, output_field=field)
update_kwargs[field.attname] = case_statement
updates.append(([obj.pk for obj in batch_objs], update_kwargs))
with transaction.atomic(using=self.db, savepoint=False):
for pks, update_kwargs in updates:
self.filter(pk__in=pks).update(**update_kwargs)
この中でも
for obj in batch_objs:
attr = getattr(obj, field.attname)
if not isinstance(attr, Expression):
attr = Value(attr, output_field=field)
when_statements.append(When(pk=obj.pk, then=attr))
case_statement = Case(*when_statements, output_field=field)
の部分で各update対象 (obj)について
When(pk=obj.pk, then=attr)
つまりSQL文における
WHEN pk = obj.pk THEN obj.attrname
を複数作成し、
when_statements.append(When(pk=obj.pk, then=attr))
でwhen_statementsなるリストに格納し
case_statement = Case(*when_statements, output_field=field)
で
CASE
WHEN pk = obj1.pk THEN obj1.attrname
WHEN pk = obj2.pk THEN obj2.attrname
...
といったクエリの部分を作成しているようです。
最後に
for pks, update_kwargs in updates:
self.filter(pk__in=pks).update(**update_kwargs)
に対応する
WHERE pk IN (obj1.pk, obj2.pk, ...)
の部分が作成されています。
よって簡易的にではありますが、
Djangoのbulk_updateが
CASE WHEN構文とWHERE ... IN構文を使用したクエリを発行することが確認できました。
なお、複数カラムが変更された時は
bulk_update内の
for field in fields:
が回っているため
UPDATE `customer`
SET `height` =
CASE
WHEN (`customer`.`id` = 1) THEN 170
WHEN (`customer`.`id` = 2) THEN 160
ELSE NULL
END,
`weight` =
CASE
WHEN (`customer`.`id` = 1) THEN 60
WHEN (`customer`.`id` = 2) THEN 50
ELSE NULL
END
WHERE (`customer`.`id` IN (1, 2))
のようなクエリになることが期待されますし、実際にDjangoのクエリ出力から期待された結果が確認できます。
結び
フレームワークのソースは複雑だという印象があり(おそらく多くの場合事実)、積極的に読む機会を積んでこなかったのですが、
今回の例のように大まかにその関数がしたいことを掴むのは簡単な場合もあるようなので、
物怖じせずにフレームワークやライブラリのコードにもコードジャンプしていこうと思いました。