Django + MySQL でバッチ的にデータの登録を行いたかったのですが、微妙にテクニカルだったので備忘メモ
bulk_insert では ID が拾えない
-
https://docs.djangoproject.com/en/4.0/ref/models/querysets/#django.db.models.query.QuerySet.bulk_create
- If the model’s primary key is an AutoField, the primary key attribute can only be retrieved on certain databases (currently PostgreSQL, MariaDB 10.5+, and SQLite 3.35+). On other databases, it will not be set.
- 要件にもよると思うが、自分は他のテーブルに登録したデータを紐付ける必要があったので、ID 取得できないのは NG
対応策:通常のインサートを行いつつ、トランザクションを広めに取る
- 通常、Django は一回の insert ごとにトランザクションを貼る。
- これをまとめることで、性能改善が望める。
検証
条件
- MacBook Pro (13-inch, 2020)
- 1.4 GHz Quad-Core Intel Core i5
- MySQL 5.7
- Django 1.11.18
サンプルコード
# ディレクトリ構成
├── tmp_multi_insert_test
│ ├── __init__.py
│ ├── models.py
│ └── script.py
├── manage.py
# models.py
from django.db import models
class test_model(models.Model):
name = models.CharField(max_length=60)
# script.py
# 100 件ごとにテスト
import timeit
from django.db import transaction
from models import test_model
def insert_in_loop(repeat=100):
ids = \[]
for i in range(repeat):
new_model = test_model(name='tmp_{}'.format(i))
new_model.save()
ids.append(new_model.id)
return ids
def insert(repeat=100, with_transaction=True):
if with_transaction:
with transaction.atomic():
ids = insert_in_loop(repeat)
else:
ids = insert_in_loop(repeat)
start = timeit.default_timer()
insert(with_transaction=False) # 引数を切り替えながらテストする
stop = timeit.default_timer()
print(stop - start)
実行方法
$ ./manage.py shel
>>> exec(open('./tmp_multi_insert_test/script.py').read())
Time: 0.49636074993759394
結果
各 10 回ぐらいずつやった平均値
トランザクションあり | トランザクションなし |
---|---|
0.094 | 0.480 |
およそ 5 倍の速度差がありました。