DjangoのORMの更新処理用にbulk_update()というメソッドが用意されている。 これは基本的にはSQLのUPDATE文を発行するのだが、通常とは違いCASE、WHEN、 THENを用いて複数の種類の値に対して1度UPDATEで更新できるようなSQLを発行 する。例えば次のようなSQLとなる。
UPDATE `fruits` SET `kind` = CASE WHEN (`fruits.id` = 1) THEN 'orange' WHEN (`fruits.id` = 2) THEN 'apple' WHEN (`fruits.id` = 3) THEN 'grape' WHERE (`fruits`.`id` IN (1, 2, 3))
これは実際に発行されたSQLではないが、イメージとしては適当なものだろう。
Djangoのドキュメントにも記載1があるが、 ドキュメントからは詳しい挙動はわからなかった。 得にこのメソッドにはfieldsとbatch_sizeという引数を用意しており、 fieldsについては指定した属性のみを更新するように思えた。 しかし実際に発行されていたSQLを眺めていると、そうも思えないクエリを発行していた。 そのため今回はこのメソッドの挙動と実際に発行されるクエリについて調べることにした。
関連するパッケージのインストール
Pythonは既にインストールしていることを前提とする。今回使用したバージョンは以下となる。
python -V
Python 3.10.6
Djangoをインストールする。venvなどの構築は必要に応じて行ってほしいが、 ここでの趣旨とは外れるため省略する。バージョンは現時点での最新を使用した。
pip install django
今回はデータベースにMySQLを使用するため、mysqlclientもインストールする。
pip install mysqlclient
以下のバージョンを使用した。
pip list
Package Version ----------- ------- asgiref 3.5.2 Django 4.1.1 mysqlclient 2.1.1 pip 22.2.2 setuptools 63.4.3 sqlparse 0.4.2
Djangoのテスト用プロジェクトの準備
Djangoのテスト用プロジェクトとしてexampleを作成する。
django-admin startproject example .
続いてテストのアプリケーションとしてappを作成する。
python manage.py startapp app
MySQLにテスト用のデータベースを作成する。
CREATE DATABASE example DEFAULT CHARACTER SET utf8mb4;
テスト用のデータベースへの接続の設定とアプリケーションの登録を行う。 example/settings.py(抜粋)::
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'example',
'HOST': 'host.docker.internal',
'PORT': 3306,
'UER': 'root',
}
}
そしてテスト用にFruitモデルを1つ定義する。
from django.db import models
class Fruit(models.Model):
kind = models.CharField(max_length=10)
number = models.IntegerField()
マイグレーションファイルを生成する。
python manage.py makemigrations
マイグレーションを実行し、テーブルを追加する。
python manage.py migrate
初期データを投入しておく
INSERT INTO `app_fruit` (`kind`, `number`) VALUES ('', 0), ('', 0), ('', 0);
データとしてこのようなデータが投入されたことを前提にする。
mysql> SELECT * FROM app_fruit; SELECT * FROM app_fruit; +----+------+--------+ | id | kind | number | +----+------+--------+ | 1 | | 0 | | 2 | | 0 | | 3 | | 0 | +----+------+--------+ 3 rows in set (0.00 sec)
これで準備は完了した。
bulk_updateが実行される際のSQLを確認する
Django Shellを用いて以下を実行する。
from app.models import Fruit
fruits = list(Fruit.objects.all())
fruits[0].kind = "orange"
fruits[0].number = 10
fruits[1].kind = "apple"
fruits[1].number = 20
fruits[2].kind = "grape"
fruits[2].number = 30
Fruit.objects.bulk_update(fruits, fields=["kind"])
最後のbulk_updateでは以下のSQLが発行される。
2022-09-10T11:37:39.194303Z 134 Query set autocommit=0 2022-09-10T11:37:39.197779Z 134 Query UPDATE `app_fruit` SET `kind` = CASE WHEN (`app_fruit`.`id` = 1) THEN 'orange' WHEN (`app_fruit`.`id` = 2) THEN 'apple' WHEN (`app_fruit`.`id` = 3) THEN 'grape' ELSE NULL END WHERE `app_fruit`.`id` IN (1, 2, 3) 2022-09-10T11:37:39.200108Z 134 Query commit 2022-09-10T11:37:39.204495Z 134 Query set autocommit=1
整形すると次のようになる。
UPDATE app_fruit SET kind = CASE WHEN (app_fruit.id = 1) THEN 'orange' WHEN (app_fruit.id = 2) THEN 'apple' WHEN (app_fruit.id = 3) THEN 'grape' END WHERE app_fruit.id IN (1, 2, 3)
データは次のように更新された。
mysql> SELECT * FROM app_fruit; SELECT * FROM app_fruit; +----+--------+--------+ | id | kind | number | +----+--------+--------+ | 1 | orange | 0 | | 2 | apple | 0 | | 3 | grape | 0 | +----+--------+--------+ 3 rows in set (0.00 sec)
期待値どおりfieldで指定した値のみ更新されることが確認できた。
おわりに
当初、挙動がおかしいと感じたものについては、 blk_update()の問題ではなく、別の要因で発生していたものだった。 実際に動かしてみて確認することで、自分の認識違いに気付けた。
脚注
Djangoのドキュメントのbulk_update()についての説明。https://docs.djangoproject.com/en/4.1/ref/models/querysets/#bulk-update