Django高速化|複数データの一括作成、更新処理方法
- 公開日:2020/04/10
- 更新日:2020/04/10
- 投稿者:n bit
Djangoでbulk_createとbulk_updateを利用したレコードデータの一括作成、一括更新方法について解説。DBへのアクセスを減らすことでDjangoの実行速度を高速化します。
この記事は約 分で読めます。(文字)
複数のデータがある場合for文などで回して一回ずつデータを作成・更新しているとデータベースへのアクセス回数が増え、Djangoアプリケーションの動作速度が非常に落ちる。そこで今回はそれらのレコードデータを一括作成、一括更新する方法について解説します。
bulk_createで一括作成方法
複数のレコードデータを一括して作成する時に利用するのはbulk_create。指定されたオブジェクトのリストをデータベースに一括で作成します。
既存のやり方
bulk_createを利用しない場合、下記のようにfor文でまわして新しいレコードを一件ずつ作成していくのが一般的でしょう。
for i in xrange(0, 100000):
ModelName.objects.create(
field_1='Test-{}'.format(i),
field_2=i,
)
数件のレコードデータでは特に問題が発生しませんが、このデータが数千件〜数万件と増えていくと実行処理にかかる時間も数分以上に伸びていきます。
Djangoのアプリケーションで、1回の処理に数分もかかっていれば実用性がありませんので、データ数が多い場合はbulk_create使った方法に変更するのがおすすめ。
bulk_createを使った複数データの一括作成
先ほどの既存コードをbulk_createを使ったやり方に置き換えると以下の通り。
create_list = []
for i in xrange(0, 100000):
obj = ModelName(
field_1='Test-{}'.format(i),
field_2=i,
)
create_list.append(obj)
ModelName.objects.bulk_create(create_list)
既存コードでは一件ずつ直接createしていましたが一度リストに格納し最後にまとめてbulk_createを使って作成します。
こちらのサンプルコードでは分かりやすさを重視して記述しましたが、せっかくDjangoの実行速度を高速化するのであればリスト内包表記を使って記述したほうがより無駄がなく良いです。
create_list = [
ModelName(
field_1='Test-{}'.format(i),
field_2=i,
)
for i in xrange(0, 100000)
]
ModelName.objects.bulk_create(create_list)
リスト内包表記の利用方法を忘れてしまった方は下記ページで詳しく解説しています。復習用にご活用ください。
Pythonのリスト内包表記の基本と使い方
Pythonでリストを生成するときに内包表記を利用するとコード内容によっては簡潔に記述することができ無駄な呼び出し処理も抑えられるので実行処理速度を上げることができます。今回はそのリスト内包表記の基本と使い方についての解説です。
既存のコードをbulk_createに変更するだけで凄く時間がかかっていた新規のデータ作成作業が数秒で終わる。bulk_createを利用しない理由はありませんね。パフォーマンス比較を行うまでもなく数十倍以上の差があります。データ数が多いのであれば必ず採用しておきましょう。
bulk_createの注意点
bulk_createにはいくつか重要な注意点があります。注意点は以下の通り。
- モデルのsave()メソッドは呼び出されず、pre_saveおよびpost_saveシグナルは送信されません。
- マルチテーブル継承シナリオの子モデルでは機能しません。
- ManyToManyFieldのリレーションは機能しません。別途、リレーションの中間テーブルをbulk_create()する必要があります。
- モデルの主キーがAutoFieldの場合、データベースバックエンドでサポートされていない限り、save()のように主キー属性を取得および設定しません(現在はPostgreSQLがサポート。PostgreSQLであれば下記のようにIDを取得することができる。)
出典:How to get primary keys of objects created using django bulk_create - Stack Overflowres_list = ModelName.objects.bulk_create(create_list)
for res in res_list:
print(res.id) - objをリストにキャストし、ジェネレーターである場合はobjを完全に評価します。キャストにより、すべてのオブジェクトを検査できるため、主キーが手動で設定されているオブジェクトを最初に挿入できます。ジェネレーター全体を一度に評価せずにオブジェクトをバッチで挿入する場合は、オブジェクトに手動で設定された主キーがない限り、この手法を使用できます。
bulk_updateで一括更新方法
複数のレコードデータを一括して更新する場合はbulk_updateを利用します。基本的な利用方法はbulk_createとよく似ていますが、1点気をつけるのはDjango2.2以上でないと標準でサポートされていない事。
以前は専用モジュールをインストールして利用する必要がありました。2.2以降は標準でサポートされたため標準でbulk_updateを利用できます。
Django2.2より前の環境で利用している方は利用することができません。専用モジュールをインストールするか、これを機にDjango2.2以上にアップデートしておきましょう。
既存のコード
複数のレコードデータを更新する場合も下記のようにループでまわして1レコードずつ更新していくのが一般的です。
for i, obj in enumrate(ModelName.objects.all()):
obj.field_1='Test-{}'.format(i),
obj.field_2=i,
obj.save()
こちらもやはり数千〜数万とデータが増えていくと実行時間が数分以上に伸びていきます。
データ数が多いのであれば、bulk_updateを積極的に利用しましょう。
bulk_updateを利用したデータ一括更新
先ほどの既存コードをbulk_updateを利用した一括更新に置き換えると以下の通り。
update_list = []
for i, obj in enumrate(ModelName.objects.all()):
obj.field_1='Test-{}'.format(i),
obj.field_2=i,
update_list.append(obj)
ModelName.objects.bulk_update(update_list, ['field_1','field_2'])
保存前のレコードデータをループ時にsave()せずリストに一旦格納し、後でbulk_updateを利用して一括更新します。
bulk_createと少し違うのは更新するフィールドを指定すること。更新対象となるフィールドをリスト形式で指定することで複数フィールドを一括して更新することも可能です。
bulk_updateをリスト内包表記を使った一括更新に置き換えると以下の通り。
update_list = [
ModelName(
id=id_,
field_1='Test-{}'.format(i),
field_2=i,
)
for i, id_ in enumerate(list(ModelName.objects.all().values_list('id', flat=True)))
]ModelName.objects.bulk_update(update_list, ['field_1','field_2'])
これでデータ数の多い更新作業は相当速くなります。
bulk_updateの注意点
bulk_updateもいくつか重要な注意点があります。注意点は以下の通り。
- モデルの主キーを更新することはできません。
- モデルのsave()メソッドは呼び出されず、pre_saveおよびpost_saveシグナルは送信されません。
- 行列数が多いデータを更新する場合、生成されるSQLサイズが非常に大きくなる可能性があります。適切なbatch_sizeを指定して、これを回避してください。
- マルチテーブルで継承元親クラスに定義されたフィールドを更新すると、親クラスごとに追加のクエリが発生します。
- objsに重複が含まれている場合、最初のもののみが更新されます。
パラメータ
bulk_create、bulk_updateそれぞれ使えるパラメータを持っています。
batch_sizeパラメータ
行列数が多いデータに対してはバッチを指定可能。batch_sizeパラメータは、1つのクエリで保存されるオブジェクト数を制御。
# bulk_create
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
・・・
# bulk_update
def bulk_update(self, objs, fields, batch_size=None):
・・・
それぞれの関数コードからもわかるようにデフォルトでは【batch_size=None】に設定されており、クエリで使用される変数の数に制限があるSQLiteとOracleを除き、すべてのオブジェクトを1つのバッチで更新します。
ignore_conflictsパラメータ
ignore_conflictsパラメータをTrueに設定すると、重複する一意の値などの制約に違反する行の挿入の失敗を無視するようにデータベースに指示。ただし、このパラメーターを有効にすると、各モデルインスタンスの主キーの設定が無効になります。
今日のdot
今回はDjangoでbulk_createとbulk_updateを利用して複数のレコードデータを一括作成、一括更新する方法について解説しました。
bulk_createとbulk_updateを利用することで比較的簡単に一括の作成・更新が行えますので大量のレコードデータを扱う場合には積極的に利用するようにしましょう。
実行にかかる処理時間は格段に変わります。