24
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Django の migration ファイルを squash してスカッとしよう!

Last updated at Posted at 2019-12-05

Djangoに限りませんが、Webアプリケーションを継続的に開発をしていくとデータベースのテーブル定義の変更が幾度となく起こり、migrationファイルが増えてきます。
migrationファイルが増えてくると、テスト実行用のデータベースを作るときに時間がかかるようになり $ python manage.py test を打ってからテストが実行されるまで暫く待つ必要がでてきます。

ちなみに scout.lapras.com のアプリケーションは 459 migrationファイルあり、テスト実行までに2分10秒かかります。(Docker for Macの上で動かしているのでそれがいくらかオーバーヘッドになっていますが)

Djangoには squashmigrations というコマンドがあり、migrationsファイルを1ファイルにまとめることができます。その際に賢くまとめてくれるので、例えば同じカラムに対する複数回の変更があっても、migrationファイルに記載されるのは最終的なカラムの情報1つだけになります。それによりALTER TABLE文の発行回数が減り migrate を高速化することに繋がります。

公式ドキュメント: https://docs.djangoproject.com/en/2.2/topics/migrations/#squashing-migrations

シンプルなアプリケーションで計測してみた

本当はこの記事を書くまでに↑のプロダクトのmigrationsをsquashしてこれだけ早くなったよ!をやりたかったのですが残念ながら間に合わなかったので、シンプルなアプリケーションを用意して計測してみました。

適当なBookモデルを用意して

from django.db import models

class Book(models.Model):
    name = models.CharField(max_length=128)
    author = models.CharField(max_length=128)
    price = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

これに対して

price = models.IntegerField(default=0)

のカラムの追加削除を繰り返すだけのmigrationファイルを50用意しました。(DBはsqlite3です)

.
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   ├── 0001_initial.py
│   ├── 0002_remove_book_price.py
│   ├── 0003_book_price.py
│   ├── 0004_remove_book_price.py
│   ├── 0005_book_price.py
│   ├── 0006_remove_book_price.py
│   ├── 0007_book_price.py
│   ├── 0008_remove_book_price.py
│   ├── 0009_book_price.py
│   ├── 0010_remove_book_price.py
│   ├── 0011_book_price.py
│   ├── 0012_remove_book_price.py
│   ├── 0013_book_price.py
│   ├── 0014_remove_book_price.py
│   ├── 0015_book_price.py
│   ├── 0016_remove_book_price.py
│   ├── 0017_book_price.py
│   ├── 0018_remove_book_price.py
│   ├── 0019_book_price.py
│   ├── 0020_remove_book_price.py
│   ├── 0021_book_price.py
│   ├── 0022_remove_book_price.py
│   ├── 0023_book_price.py
│   ├── 0024_remove_book_price.py
│   ├── 0025_book_price.py
│   ├── 0026_remove_book_price.py
│   ├── 0027_book_price.py
│   ├── 0028_remove_book_price.py
│   ├── 0029_book_price.py
│   ├── 0030_remove_book_price.py
│   ├── 0031_book_price.py
│   ├── 0032_remove_book_price.py
│   ├── 0033_book_price.py
│   ├── 0034_remove_book_price.py
│   ├── 0035_book_price.py
│   ├── 0036_remove_book_price.py
│   ├── 0037_book_price.py
│   ├── 0038_remove_book_price.py
│   ├── 0039_book_price.py
│   ├── 0040_remove_book_price.py
│   ├── 0041_book_price.py
│   ├── 0042_remove_book_price.py
│   ├── 0043_book_price.py
│   ├── 0044_remove_book_price.py
│   ├── 0045_book_price.py
│   ├── 0046_remove_book_price.py
│   ├── 0047_book_price.py
│   ├── 0048_remove_book_price.py
│   ├── 0049_book_price.py
│   ├── 0050_remove_book_price.py
│   └── __init__.py
├── models.py
└── tests.py

これに対してテストを実行すると…

$ time python ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
Destroying test database for alias 'default'...
python ./manage.py test  0.73s user 0.20s system 73% cpu 1.261 total

なんと0.73秒で終わってしまいました。。。
僕の想像ではこれで少なくとも5秒ぐらいかかって、squashすると0.5秒ぐらいになる想定だったのですが、、、
先行きが怪しくなります。。

squashしてみます。

$ python manage.py squashmigrations squash_test 0050_remove_book_price
Will squash the following migrations:
 - 0001_initial
 - 0002_remove_book_price
 - 0003_book_price
()
 - 0050_remove_book_price
Do you wish to proceed? [yN] y

Optimizing...
  Optimized from 50 operations to 1 operations.
Created new squashed migration /django-squash-test/app/squash_test/migrations/0001_squashed_0050_remove_book_price.py
  You should commit this migration but leave the old ones in place;
  the new migration will be used for new installs. Once you are sure
  all instances of the codebase have applied the migrations you squashed,
  you can delete them.

これですべてが詰まった1つのmigrationファイルができました。
前後を比較すると

before

0001_initial.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-12-05 12:13
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Book',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=128)),
                ('author', models.CharField(max_length=128)),
                ('price', models.IntegerField()),
                ('created_at', models.DateTimeField(auto_now_add=True)),
            ],
        ),
    ]
0002_remove_book_price.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-12-05 12:14
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('squash_test', '0001_initial'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='book',
            name='price',
        ),
    ]
0003_book_price.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-12-05 12:14
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('squash_test', '0002_remove_book_price'),
    ]

    operations = [
        migrations.AddField(
            model_name='book',
            name='price',
            field=models.IntegerField(default=0),
            preserve_default=False,
        ),
    ]

(以下、2と3と同じファイルの繰り返し)

after

0001_squashed_0050_remove_book_price.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-12-05 13:53
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    replaces = [('squash_test', '0001_initial'), ('squash_test', '0002_remove_book_price'), ('squash_test', '0003_book_price'), ('squash_test', '0004_remove_book_price'), ('squash_test', '0005_book_price'), ('squash_test', '0006_remove_book_price'), ('squash_test', '0007_book_price'), ('squash_test', '0008_remove_book_price'), ('squash_test', '0009_book_price'), ('squash_test', '0010_remove_book_price'), ('squash_test', '0011_book_price'), ('squash_test', '0012_remove_book_price'), ('squash_test', '0013_book_price'), ('squash_test', '0014_remove_book_price'), ('squash_test', '0015_book_price'), ('squash_test', '0016_remove_book_price'), ('squash_test', '0017_book_price'), ('squash_test', '0018_remove_book_price'), ('squash_test', '0019_book_price'), ('squash_test', '0020_remove_book_price'), ('squash_test', '0021_book_price'), ('squash_test', '0022_remove_book_price'), ('squash_test', '0023_book_price'), ('squash_test', '0024_remove_book_price'), ('squash_test', '0025_book_price'), ('squash_test', '0026_remove_book_price'), ('squash_test', '0027_book_price'), ('squash_test', '0028_remove_book_price'), ('squash_test', '0029_book_price'), ('squash_test', '0030_remove_book_price'), ('squash_test', '0031_book_price'), ('squash_test', '0032_remove_book_price'), ('squash_test', '0033_book_price'), ('squash_test', '0034_remove_book_price'), ('squash_test', '0035_book_price'), ('squash_test', '0036_remove_book_price'), ('squash_test', '0037_book_price'), ('squash_test', '0038_remove_book_price'), ('squash_test', '0039_book_price'), ('squash_test', '0040_remove_book_price'), ('squash_test', '0041_book_price'), ('squash_test', '0042_remove_book_price'), ('squash_test', '0043_book_price'), ('squash_test', '0044_remove_book_price'), ('squash_test', '0045_book_price'), ('squash_test', '0046_remove_book_price'), ('squash_test', '0047_book_price'), ('squash_test', '0048_remove_book_price'), ('squash_test', '0049_book_price'), ('squash_test', '0050_remove_book_price')]

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Book',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=128)),
                ('author', models.CharField(max_length=128)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
            ],
        ),
    ]

になりました。
実行時間は

$ time python ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
Destroying test database for alias 'default'...
python ./manage.py test  0.52s user 0.09s system 98% cpu 0.611 total

0.52秒で、50ファイルあったときの71%になりました! (微妙ですが減ってくれてよかった…)

squash された migration ファイルの解説

公式ドキュメント https://docs.djangoproject.com/en/2.2/topics/migrations/#squashing-migrations にも書かれているので、より詳細な情報は原文を読んで頂くのがよいですが、簡単に説明します。

operations の部分は見慣れた構文と同じですが、普段のmigrationファイルでは見ない replaces という変数が新たに定義されています。
これはどのmigrationsファイルたちをまとめてこのmigrationファイルを作ったかという情報になります。

注意点として、squashされたmigrationファイルができたからといってsquash元のmigrationファイルたちをすぐには削除できません。すでに部分的にmigrationが当たっている環境、 例えば、0010_remove_book_price までは実行済みの環境があった場合、残りの 0011_book_price から 0050_remove_book_price までを当てる必要があり、この場合にはsquashしたmigrationファイルだけでは対応できないためです。

replaces の中に含まれているmigrationファイルすべてが未適用な環境であれば個別のmigrationファイルの代わりにsquashされたmigrationファイルが使用されより高速にmigrateすることができます。

自動でsquashできないケース

Djangoでは複数アプリケーションを一つのプロジェクトとして管理できます。
大規模なDjangoアプリケーションではドメインごとにアプリケーションを分けて、数十のアプリケーションを1プロジェクトで持つような形がベストプラクティスの一つとしてあります。

このような場合にはアプリケーションを跨いで外部キー制約をつけることが多々発生し、migrationファイル同士の依存関係もかなり複雑になります。
必ずその依存先のmigrationファイルの後でなければ動かないというわけではないのに、djangoが自動で張った依存関係の影響でスマートにsquashできない場合があります。

文字だと難しいので例を出すと
1. app1.Book 作成: 0001_app1
2. app2.Category 作成: 0001_app2
3. app1.Magazine 作成: 0002_app1 (0001_app1 に依存)
4. app2.Category のカラム変更: 0002_app2 (0001_app2 に依存)
5. app1.Book から Category へのF-Key追加: 0003_app1 (0002_app10002_app2 に依存)

という状況でsquashすることを考えた場合に、squashmigrations をすると 0001_app10002_app10001_app20002_app2 がまとめられ、0003_app1 はそのまま残り合計3ファイルになります。

ただこれは最適解ではなくて、人間が考えれば 0001_app20002_app2 をまとめて、0001_app10002_app20003_app1 をまとめて、後者が前者に依存することで合計2ファイルにまとめられることがわかります。

そして大規模なアプリケーションになると往々にしてこのような問題が起き、自動で squashmigrations するだけではあまりmigrationファイルが減らず、人間が頭を捻りながら微調整をしていく必要が出てきてしまいます。
微量な高速化のために人間がどれだけコストを払うのかはよく考えましょう。

最後に

僕は高速化が趣味なのでsquashしてこれぐらい早くなった〜!と速度比較をするのが大好きでsquashしたあとはスカッとします。
ちなみに、最近はスポーツのスカッシュにもハマっていて、来週末には初めて大会に出場することになりました。そこでも勝利してスカッとできるといいなと思っています。

24
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?