既存の複数のレコードをまとめて更新する際にdjango-bulk-updateというパッケージが便利だったので紹介します。
GitHub - aykut/django-bulk-update: Bulk update using one query over Django ORM
通常のDjangoのORMを使ったレコードの更新
Djangoで既存のレコードを更新するには、モデルインスタンスのsaveメソッドを呼ぶか、クエリセットのupdateを使うのが通常の方法です(Django1.9時点)
class Item(models.Model):
value = models.CharField(max_length=20)
class Meta:
db_table = 'item'
Item.objects.create(pk=1, value="spam")
item = Item.objects.get(pk=1)
item.value = "更新された値"
item.save()
Item.objects.filter(pk=1).update(value="更新された値1")
これは、バックエンドにもよりますが、例えばMySQLの場合だとUPDATEクエリが実行されます。
複数件をまとめて更新する場合に、更新後の値がすべて同じであれば、クエリは単純でDjangoの標準ORMでも対応できます。
Item.objects.create(pk=1, value="spam")
Item.objects.create(pk=2, value="egg")
Item.objects.create(pk=3, value="ham")
Item.objects.filter(pk__in=[1, 2, 3]).update(value="更新された値")
しかし、一件ごとに違う内容で保存したい場合には、モデルインスタンスのsaveメソッドか、前述のupdateメソッドで何度もUPDATEクエリを実行することになります。
大量に更新する場合には、これだと時間がかかってしまいます。
raw SQLで対応するならどうやるか
例えばMySQLの場合、レコードの更新方法だと、UPDATE文以外にREPLACE文などもあります。またLOAD DATA INFILEでREPLACEを行う方法もあります。
ただしこれは、既存のレコードを削除した上で更新となるため、一部の列の内容だけを更新したい場合には、事前にSELECTした値を使う必要があったりして少し使いづらいことがあります。
更新対象のテーブルに主キー、または一意なキーがある前提の場合、UPDATE文のSETの値部分にCASE演算子を指定することで、一回のクエリの発行でそれぞれ別の値を更新できます。
mysql> SELECT * FROM item;
+
| id | value |
+
| 1 | spam |
| 2 | egg |
| 3 | ham |
+
3 rows in set (0.00 sec)
mysql> UPDATE item SET value=(
-> CASE id
-> WHEN 1 THEN 'updated_spam'
-> WHEN 2 THEN 'updated_egg'
-> WHEN 3 THEN 'updated_ham'
-> END)
-> WHERE id in (1, 2, 3);
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3 Changed: 3 Warnings: 0
mysql> SELECT * FROM item;
+
| id | value |
+
| 1 | updated_spam |
| 2 | updated_egg |
| 3 | updated_ham |
+
3 rows in set (0.00 sec)
他にもまとめて更新する方法はあるかと思いますが、ここでは言及しません。
django-bulk-updateを使う
django-bulk-updateを使うと、前述のUPDATEのSET部分にCASE演算子を使うクエリの発行をSQLの記述無しでできます。
PyPI上のパッケージ名はdjango-bulk-updateです。
django-bulk-update · PyPI
(venv)$ pip install django-bulk-update
使い方は、Djangoの標準ORMのbulk_createに似ています。
Django1.9、Python3.5、django-bulk-update1.1.8で試しました。
from bulk_update.helper import bulk_update
def my_update_task():
updates = [
Item(pk=1, value="updated_spam"),
Item(pk=2, value="updated_egg"),
Item(pk=3, value="updated_ham"),
]
bulk_update(updates, update_fields=['value'])
print(Item.objects.values_list())
実行結果は次の通り。
>>> my_update_task()
[(1, 'updated_spam'), (2, 'updated_egg'), (3, 'updated_ham')]
MySQLの場合、実行されたクエリは次の通りです。
UPDATE `item` SET `value` = (CASE `id` WHEN 1 THEN 'updated_spam' WHEN 2 THEN 'updated_egg' WHEN 3 THEN 'updated_ham' END) WHERE id in (1, 2, 3)
このようにbulk_updateを使うことで、1クエリで複数件の更新を簡単にできるようになりました。便利。