DjangoのBulk Updateの挙動をソースを追って確認する


# はじめに
[DjangoでDBを一括更新する](https://qiita.com/risaitou4329/items/726b3c7dc17ac9404984 "Qiita Home")
の記事では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のクエリ出力から期待された結果が確認できます。


# 結び
フレームワークのソースは複雑だという印象があり(おそらく多くの場合事実)、積極的に読む機会を積んでこなかったのですが、
今回の例のように大まかにその関数がしたいことを掴むのは簡単な場合もあるようなので、
物怖じせずにフレームワークやライブラリのコードにもコードジャンプしていこうと思いました。