複数のレコードをまとめて更新する際にdjango-bulk-updateが便利

既存の複数のレコードをまとめて更新する際に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クエリで複数件の更新を簡単にできるようになりました。便利。

2019/2/28追記

Django 2.2で同等の機能となるbulk_updateメソッドが実装されます。

django.db.models.query.QuerySet.bulk_update