Djangoで、データの一括作成・一括更新

/ PythonDjango

21日前に更新

概要

Djangoでのバルクインサート・バルクアップデートの方法を紹介します。

例えば、1万件のデータを一括で登録したいとしましょう。Django、CSVのインポート・エクスポート のようなCSVからの一括作成であったり、ちょっとしたテストデータの登録かもしれません。

素直にやると、1万件のデータは次のように作れます

for i in range(10000):
    Post.objects.create(title=f'{i}件目のデータ')

これは非常に遅いです。試してみましたが、中々終わらないので途中でやめました。1000件のデータでさえ、私の環境では3分近くかかっています。

1万、10万件といったレベルになってくると、この方法はつらいです。

bulk_createの使い方

bulk_create()を使えば、次のように書けます。

posts = []
for i in range(100000):
    post = Post(title=f'{i}件目のデータ')
    posts.append(post)
Post.objects.bulk_create(posts)

モデル名.objects.bulk_create(保存前モデルインスタンスのリスト) という書式ですね。

今回のモデルはタイトルフィールドしかないシンプルなものですが、10万件登録しても3秒前後で終わりました。比べるのがアホらしいほどに早いですね。

一点注意として、モデルのsave()メソッドやpre_save, post_saveシグナルは動作しないことを覚えておきましょう。

ForeignKey

モデルが次のようになったとします。ForeignKeyで、カテゴリモデルと紐づきます。

from django.db import models


class Category(models.Model):
    name = models.CharField('カテゴリ', max_length=255)


class Post(models.Model):
    title = models.CharField('記事タイトル', max_length=255)
    category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='カテゴリ')

この場合でも殆ど変わらず、次のように作れます。

# カテゴリAを取得
category = Category.objects.get(name='食べ物')

# 記事の一括作成。カテゴリは全て「食べ物」
posts = []
for i in range(100000):
    post = Post(title=f'{i}件目のデータ', category=category)
    posts.append(post)
Post.objects.bulk_create(posts)

ManyToManyField

次のように、ManyToManyFieldで紐づくようになりました。

from django.db import models


class Tag(models.Model):
    name = models.CharField('カテゴリ', max_length=255)


class Post(models.Model):
    title = models.CharField('記事タイトル', max_length=255)
    tags = models.ManyToManyField(Tag, verbose_name='カテゴリ')

残念ながら、ManyToManyFieldを伴うバルクインサートは正式にサポートされていませんので、少し工夫する必要があります。

ManyToManyFieldを使ったとき、Djangoは次のような中間テーブルを作成して管理します。
中間テーブル

post_id tag_id
132 1
132 2

のようになっていますね。これは、記事id132のデータ(この記事)が、タグid1(Pythonタグ)とタグid2(Djangoタグ)と紐づいていることを意味します。

なので、このテーブルに直接追加してしまえば良い訳です。この中間テーブルは、Post.tags.throughのようにしてアクセスできます。

# 紐づけたいタグを取得
tag_a = Tag.objects.get(name='食べ物')
tag_b = Tag.objects.get(name='飲み物')

# 記事の一括作成。タグはこの時点では指定しない
posts = []
for i in range(100000):
    post = Post(title=f'{i}件目のデータ')
    posts.append(post)
Post.objects.bulk_create(posts)

# タグの一括作成
# 結果的に、全ての記事にタグAとタグBを追加している
Through = Post.tags.through
through_list = []
for post in Post.objects.all():
    through_list.append(Through(post_id=post.pk, tag_id=tag_a.pk))
    through_list.append(Through(post_id=post.pk, tag_id=tag_b.pk))
Through.objects.bulk_create(through_list)

bulk_updateの使い方

Django2.2から、一括更新のためのbulk_update()がサポートされました。以前は、専用のライブラリを導入する必要がありました。

次のコードは、追加した全ての記事のタイトルの末尾に「でござる」をつけて更新する例です。

posts = []
for i in range(100000):
    post = Post(title=f'{i}件目のデータ')
    posts.append(post)
Post.objects.bulk_create(posts)

posts = []
for post in Post.objects.all():
    post.title = post.title + 'でござる'
    posts.append(post)
Post.objects.bulk_update(posts, fields=['title'])

書式はbulk_create()と殆ど同じです。postsの部分は、更新したい保存済みのモデルインスタンスのリストです。

fields引数には、更新対象のフィールドをリストで指定します。

この記事の関連記事

コメント欄

記事にコメントする

名無し

いつもブログの方参考にさせていただいております。 ありがとうございます。

Django2.2からサポートされた bulk_update() について伺いたいのですが bulk_update() で中間テーブルthroughを一括更新することは可能でしょうか?

更新すべきidの抽出等よくわかりませんでした。

コメントに返信する

なりと

そんなに使うことはないと思いますが、可能です。

次のコードは、記事に紐づいている全ての食べ物タグ(food_tag)を、飲み物タグ(drink_tag)に変更する例です。

Through = Post.tags.through
through_list = []

# 中間テーブル内のデータのうち、食べ物カテゴリ(food_tag)のものだけ取得
for thr in Through.objects.filter(tag_id=food_tag.pk):
    # そのデータが指しているタグIDを飲み物カテゴリ(drink_tag)に変更したインスタンスを作る
    through_list.append(Through(pk=thr.pk, tag_id=drink_tag.pk))

Through.objects.bulk_update(through_list, fields=['tag_id'])
名無し

コード付きの丁寧なご解説ありがとうございます。

1 - 1 の一括変更は行えました。

(1 - 1)全ての食べ物タグ(food_tag)を、飲み物タグ(drink_tag)に変更

ではなく

(1 - n)全ての食べ物タグ(food_tag)を、飲み物タグ(drink_tag)と、デザートタグ(dessert_tag)に変更の場合(=食べ物タグをもったpostを、飲み物タグと、デザートタグに一括変更したい)でも対応できるのでしょうか?

現状は下記のように対応しているのですが件数が多い場合にはbulk_updateかと思います。

tags =Tag.objects.filter(name__in=['飲み物', 'デザート'])

for post in Post.objects.all():
    post.tag.set(tags)

(1 - 1)から(1 - n)に一括変更する場合は中間テーブルが足りなくなるので 新規に追加作成するイメージでしょうか?

なりと

その場合はbulk_create()も使うことになると思います。

Through = Post.tags.through
update_through_list = []  # bulk_update用リスト
create_through_list = []  # bulk_create用リスト

for thr in Through.objects.filter(tag_id=food_tag.pk):
    # 食べものタグを飲み物タグに変更
    update_through_list.append(
        Through(pk=thr.pk, tag_id=drink_tag.pk)
    )

    # その記事に紐づいたデザートタグを新たに追加する
    create_through_list.append(
        Through(post_id=thr.post_id, tag_id=dessert_tag.pk)
    )

Through.objects.bulk_update(update_through_list, fields=['tag_id'])
Through.objects.bulk_create(create_through_list)
名無し

ありがとうございます。

切り分けながらやってみます。

コードを記述しながら(n - 1)には対応でき無いことに気がつきました。

おっしゃる通り利用出来るケースは限られますね。