Djangoで、データの一括作成・一括更新
|PythonDjango概要
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
引数には、更新対象のフィールドをリストで指定します。
関連記事
Django、CSVのインポート・エクスポート
Webアプリケーションにおいて、CSVとの連携機能はよく必要とされる機能です。今回はDjangoで、DB ⇔ CSV なやりとりをしていきます
PythonDjangocsvDjangoで、初期データの投入
データベースに予めデータを用意しておきたい、という場合があります。ロジック上必要な初期データや、テスト用のデータ等がそれです。今回は、Djangoで初期データを投入する方法を紹介していきます。
PythonDjango
コメント一覧
2019/3/28 名無し
いつもブログの方参考にさせていただいております。 ありがとうございます。
Django2.2からサポートされた bulk_update() について伺いたいのですが bulk_update() で中間テーブルthroughを一括更新することは可能でしょうか?
更新すべきidの抽出等よくわかりませんでした。
2019/3/29 なりと
そんなに使うことはないと思いますが、可能です。
次のコードは、記事に紐づいている全ての食べ物タグ(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'])
2019/3/29 名無し
コード付きの丁寧なご解説ありがとうございます。
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)に一括変更する場合は中間テーブルが足りなくなるので 新規に追加作成するイメージでしょうか?
2019/3/29 なりと
その場合は
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)
2019/3/30 名無し
ありがとうございます。
切り分けながらやってみます。
コードを記述しながら(n - 1)には対応でき無いことに気がつきました。
おっしゃる通り利用出来るケースは限られますね。
↑返信する
新しくコメントする