6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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のクエリ出力から期待された結果が確認できます。

結び

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?