既存の複数のレコードをまとめて更新する際にdjango-bulk-updateというパッケージが便利だったので紹介します。
GitHub - aykut/django-bulk-update: Bulk update using one query over Django ORM
通常のDjangoのORMを使ったレコードの更新
Djangoで既存のレコードを更新するには、モデルインスタンスのsaveメソッドを呼ぶか、クエリセットのupdateを使うのが通常の方法です(Django1.9時点)
# Itemモデル class Item(models.Model): value = models.CharField(max_length=20) class Meta: db_table = 'item' # 新規作成(id=1で保存) Item.objects.create(pk=1, value="spam") # id=1のレコードの更新(saveメソッド) item = Item.objects.get(pk=1) item.value = "更新された値" item.save() # id=1のレコードの更新(QuerySetのupdateメソッド) Item.objects.filter(pk=1).update(value="更新された値1")
これは、バックエンドにもよりますが、例えばMySQLの場合だとUPDATEクエリが実行されます。
複数件をまとめて更新する場合に、更新後の値がすべて同じであれば、クエリは単純でDjangoの標準ORMでも対応できます。
# 新規作成(id=1, 2, 3で保存) Item.objects.create(pk=1, value="spam") Item.objects.create(pk=2, value="egg") Item.objects.create(pk=3, value="ham") # id=1, 2, 3のレコードの更新(すべて同じ値にする) 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(): # 更新対象のインスタンスを用意(明示的に生成せずに、QuerySetで取得したインスタンスでも可) updates = [ Item(pk=1, value="updated_spam"), Item(pk=2, value="updated_egg"), Item(pk=3, value="updated_ham"), ] # value列だけを更新 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クエリで複数件の更新を簡単にできるようになりました。便利。