PythonDjango

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

概要

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引数には、更新対象のフィールドをリストで指定します。