マイグレーション¶
マイグレーション (Migrations) は、Django でモデルに対して行った変更 (フィールドの追加やモデルの削除など) をデータベーススキーマに反映させる方法です。大抵のマイグレーションは自動で行われるものの、いつマイグレーションが作られ、いつ実行され、どんな問題がよく起こるのかは、知っておいた方がいいでしょう。
コマンド¶
マイグレーションと Django のデータベーススキーマの操作に関わる時によく使うコマンドを、いくつか挙げておきましょう。
migrateは、マイグレーションを適用したり、適用をキャンセルするのに使います。makemigrationsは、モデルに対して行った変更をもとに、新しいマイグレーションを作成します。sqlmigrateは、マイグレーションに対応する SQL 文を表示します。showmigrationsは、プロジェクトのマイグレーションとそのステータスをリストします。
マイグレーションというのは、データベーススキーマに対するバージョン管理システムのようなものです。makemigrations はモデルの変更点を1つのマイグレーションファイルにパッケージングし(コミットのようなものです)、migrate はその変更点をデータベースに適用する、というわけです。
各アプリのマイグレーションファイルはそのアプリの "migrations" ディレクトリの中に保管され、コードベースの一部としてコミットされ、配布されるようにデザインされています。いったん開発用マシンでマイグレーションファイルが作成されれば、その後、チームメンバーのマシンやステージング環境のマシン上で同一のマイグレーションが行われ、最終的にプロダクション環境でも同じマイグレーションが行われます。
注釈
アプリのパッケージの名に migrations が含まれ、上書きされてしまう場合には、設定ファイルの MIGRATION_MODULES を修正してください。
マイグレーションが同じデータセットに同じ方法で実行され、一貫した結果を生み出すということは、開発環境やステージング環境下で目にする結果が、プロダクション環境下でも全く同じになるということです。
Django は、モデルやフィールドに変更があった場合、たとえデータベースに影響を与えないオプションであっても、マイグレーションを行います。フィールドを正しく再構築する唯一の方法は、すべての変更が履歴に残っていることであり、後々のデータマイグレーションで、これらのオプションが必要になるかもしれないからです (たとえば、カスタムバリデータを設定した場合など)。
対応するバックエンド¶
マイグレーションは Django で標準で利用できるすべてのバックエンドに対応しています。サードパーティ製のバックエンドでも、プログラムからのスキーマの変更の操作(SchemaEditor クラスで実行される)に対応していれば大丈夫です。
しかし、スキーマのマイグレーションは、データベースによってが得手・不得手があります。注意点を以下で説明します。
PostgreSQL¶
PostgreSQLはスキーマのサポートという点で、ここにあるすべてのデータベースの中で最も有能です。
MySQL¶
MySQL はスキーマの変更操作周りのトランザクションをサポートしていません。つまり、マイグレーションの適用が失敗した場合には、手動で変更点を調べあげ、やり直さなければならないということです (過去の時点にロールバックすることは不可能ということです)。
MySQL 8.0 では DDL operations のパフォーマンスが大幅に強化され、より効率的になり、テーブルの完全な再構築の必要性が減りました。しかし、ロックや中断が完全に発生しないことは保証されません。ロックが必要な場合、操作時間は対象の行数に比例して長くなります。
最後に、MySQL ではインデックスがカバーするすべてのカラムの合計サイズに比較的小さな制限があります。これは、他のバックエンドでは可能なインデックスが MySQL では作成できないことを意味します。
SQLite¶
SQLite はビルトインのスキーマ変更操作をほとんどサポートしていません。そのため、Django は以下のようにしてスキーマの変更動作をエミュレートします。
新しいスキーマで、新しいテーブルを作成する
テーブル間でデータをコピーする
古いテーブルを削除する
新しいテーブルを元のテーブル名に変更する
この方法で大抵はうまくいきますが、遅かったりたまにテーブルが壊れてしまうことがあります。そのため、SQLite をプロダクション環境で使用するのは、このリスクと制限を十分理解している場合以外には、おすすめしません。Django がデフォルトで SQLite を使用しているのは、開発者が SQLite をローカルマシンでかんたんに実行できるようにすることで、本格的なデータベースがなくても Django のプロジェクトが開発できるようにして、複雑さを除くようにデザインされているからです。
ワークフロー¶
Django はあなたに代わってマイグレーションを作成します。たとえば、フィールドを追加してモデルを削除するなど、モデルに変更を加えてから、makemigrations を実行してください。
$ python manage.py makemigrations
Migrations for 'books':
books/migrations/0003_auto.py:
~ Alter field author on book
すると、あなたが書いたモデルがスキャンされ、現在のバージョンのマイグレーションファイルに記録されているモデルと比較されます。この時、makemigrations があなたが何を変更したと考えているのかを理解するために、出力をよく読んでください。不完全だったり、意図したよりも複雑な結果が表示されるかもしれません。
問題なく新しいマイグレーションファイルが生成されたら、期待通りに変更が行われるように、データベースに適用します:
$ python manage.py migrate
Operations to perform:
Apply all migrations: books
Running migrations:
Rendering model states... DONE
Applying books.0003_auto... OK
マイグレーションを適用したら、マイグレーションとモデルの変更をバージョン管理システムに1つのコミットとしてコミットしましょう。こうすることで、他の開発者 (やプロダクションサーバー) がコードをチェックアウトした時に、モデルの変更とマイグレーションの適用を同時に実行できます。
マイグレーションに、自動生成された名前ではなく、意味のある名前を与えたければ、makemigrations --name オプションが使えます。
$ python manage.py makemigrations --name changed_my_model your_app_label
バージョン管理¶
マイグレーションはバージョン管理システムに保管されるため、あなたがマイグレーションをコミットしたのと同じアプリに、同じタイミングで、他の開発者もマイグレーションをコミットしてしまい、結果として同じ数字のマイグレーションが2つできてしまう、というシチュエーションに遭遇するかもしれません。
でも、大丈夫。マイグレーションの数字は開発者が参考にするために付けられているだけなのです。Django が気にするのは、マイグレーションの名前が異なっているかどうかだけです。マイグレーションはファイルの中で、自分が依存している他のマイグレーション (同じアプリの過去のマイグレーションを含む) を明記しています。そのため、同じアプリへの2つの順序関係がない新しいマイグレーションが存在していれば、それらをちゃんと検出できます。
このような状況が起きた場合、Django はいくつかの選択肢を提示します。それを読んで十分安全だと判断できれば、2つのマイグレーションを自動的に2つの連続するマイグレーションに変更してくれます。そうでなければ、マイグレーションファイルを自分で修正する必要があります。でも、難しくないので心配はいりません。詳しくは、下の マイグレーションファイル で説明しています。
トランザクション¶
DDL トランザクションをサポートしているデータベース (SQLite や PostgreSQL) では、すべてのマイグレーション操作はデフォルトで単一のトランザクション内で実行されます。一方、データベースが DDL トランザクションをサポートしていない場合 (MySQL や Oracle など)、すべての操作はトランザクションなしで実行されます。
マイグレーションをトランザクション内で実行しないようにするには、atomic 属性を False に設定します。たとえば次のようにします。
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
atomic() を使うか、 RunPython に atomic=True を渡すことで、マイグレーションの一部をトランザクション内で実行することもできます。詳しくは 非アトミックのマイグレーション を参照してください。
依存関係¶
マイグレーションはアプリごとに作られますが、モデルが表しているテーブルやリレーションシップは、一度に一つのアプリに対して作るには複雑すぎます。他のマイグレーションの実行を要求するマイグレーション、たとえば books アプリ内の ForeignKey を authors アプリに加えるような場合、結果として作られるマイグレーションには、authors のマイグレーションへの依存関係が生じます。
つまり、マイグレーションを実行すると、最初に authors のマイグレーションが実行されて ForeignKey リファレンスが参照するテーブルが作成され、その後で ForeignKey カラムを作るマイグレーションが実行された後、制約が作られます。もしそうでなかったら、books のマイグレーションが存在しないテーブルを参照する ForeignKey カラムを作ろうとして、結果、データベースはエラーを出してしまうでしょう。
この依存関係の動作は、1つのアプリに制限するマイグレーション操作のほとんどに影響します。(makemigrations または migrate で)1つのアプリに制限することは最善の努力であり、保証ではありません。
マイグレーションをしていないアプリは、マイグレーションをしているアプリとリレーション (ForeignKey、ManyToManyField など) を持ってはいけません。うまくいくこともありますが、サポートはされていません。
スワップ (交換) 可能な依存関係¶
- django.db.migrations.swappable_dependency(value)¶
swappable_dependency() 関数はマイグレーションで使用され、スワップインモデルのアプリでマイグレーションに対する「スワップ可能な」依存関係を宣言します。結果として、スワップインモデルは最初のマイグレーションで作成される必要があります。引数 value には、アプリのラベルとモデル名を表す文字列 "<app label>.<model>" を指定します。たとえば "myapp.MyModel" のように指定します。
swappable_dependency() を使用することで、マイグレーションがスワップ可能なモデルを設定する別のマイグレーションに依存していることをマイグレーションフレームワークに通知し、将来的にモデルを別の実装で置き換えることができるようにします。これは通常、Django の認証システムのカスタムユーザモデル (settings.AUTH_USER_MODEL、デフォルトは "auth.User") のような、カスタマイズや置き換えの対象となるモデルを参照するために使われます。
マイグレーションファイル¶
マイグレーションはディスク上のファイルとして保存されます。ここではそれを「マイグレーションファイル」と呼びます。このファイルは実際には、決まった仕方でオブジェクトを配置した、宣言型のスタイルで書かれたふつうの Python ファイルです。
基本的なマイグレーションファイルは、次のような形式です。
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("migrations", "0001_initial")]
operations = [
migrations.DeleteModel("Tribble"),
migrations.AddField("Author", "rating", models.IntegerField(default=0)),
]
Django が (Python モジュールとして) マイグレーションファイルを読み込んだ時に最初に探すのは、Migration という名前の django.db.migrations.Migration のサブクラスです。そして、このサブクラスの4つの属性を調べますが、ほとんど場合に使われるのは、次の2つの属性です。
dependenciesは、このマイグレーションが依存する他のマイグレーションのリストです。operationsは、このマイグレーションが行う操作を定義しているOperationクラスのリストです。
operations がポイントです。これは、宣言的な命令の集まりで、Django にどんなスキーマの変更が必要かを教えます。Django はそれらをスキャンして、全アプリへのスキーマの変更を完全に表現するデータ構造をメモリ上に作り上げ、これを利用して、Django スキーマを実際に変化させる SQL 文を生成します。
この時に作られるメモリ上のデータ構造は、新しいモデルと現在のマイグレーションの状態の差分を計算するのにも使われます。Django は、メモリ上のモデルの集まりのすべての変更点を順番にたどって行き、最後に makemigrations した時のモデルの状態を理解します。そして、そのモデルと models.py ファイルにあるモデルとを比較し、行った変更に対して作業を行うのです。
ごく稀にマイグレーションファイルを手で修正しなければならないことがありますが、必要があればすべて手で書くことも特に難しい作業ではありません。複雑なデータベース操作の中には自動的には検出できないものもあり、その場合にはマイグレーションを手で書くことが必須になることがあります。でも必要な場合には、自分の手で書くのを怖がらないでください。
カスタムフィールド¶
すでにマイグレートしたカスタムのフィールドの位置引数の数を変更しようとすると、TypeError が発生してしまします。古いマイグレーションは、修正した __init__ メソッドを古い引数で呼んでしまいます。そこで、新しい引数が必要な場合は、キーワード引数を作り、コンストラクタ内に assert 'argument_name' in kwargs のような一文を追加してください。
モデルマネージャ¶
オプションとして、マネージャをマイグレーションにシリアライズして、RunPython の中で使えるようにすることができます。それには次のように、マネージャクラスの中で use_in_migrations 属性を定義します。
class MyManager(models.Manager):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
もし from_queryset() 関数で動的に生成されたマネージャクラスを使うなら、インポートできるように生成されたクラスを継承する必要があります。
class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
それに伴う影響については、マイグレーションにおける 履歴上のモデル のメモも参考にしてください。
初期マイグレーション¶
- Migration.initial¶
アプリの「初期マイグレーション」とは、そのアプリのテーブルの最初のバージョンを作成するマイグレーションです。通常、アプリの初期マイグレーションは1つですが、モデルの相互依存関係が複雑な場合は2つ以上になることもあります。
初期マイグレーションはマイグレーションクラスの initial = True クラス属性でマークされます。もし initial クラス属性が見つからない場合、マイグレーションはアプリ内で最初のマイグレーションであれば(つまり、同じアプリ内の他のマイグレーションに依存していなければ)、"初期 "とみなされます。
migrate --fake-initial オプションを使うと、これらの初期マイグレーションは特別に扱われます。1 つ以上のテーブルを作成するマイグレーション (CreateModel オペレーション) では、 Django はそれらのテーブルが既にデータベースに存在するかどうかをチェックし、存在する場合にはマイグレーションをフェイク適用します。同様に、1つ以上のフィールドを追加する最初のマイグレーション (AddField オペレーション) では、 Django はそれぞれのカラムが既にデータベースに存在するかどうかをチェックし、存在すればマイグレーションをフェイク適用します。 --fake-initial がなければ、初期マイグレーションは他のマイグレーションと同じように扱われます。
履歴の一貫性¶
前に説明したように、2つの開発ブランチを結合するときに、マイグレーションを手動で線形化する必要があるかもしれません。マイグレーションの依存関係を編集しているときに、マイグレーションは適用されたのに、 その依存関係のいくつかは適用されていないというような、一貫性のない履歴状態をうっかり作ってしまうことがあります。これは依存関係が正しくないことを強く示しているので、 Django はそれが修正されるまでマイグレーションを実行したり、新しいマイグレーショ ンを作成したりすることを拒否します。複数のデータベースを使う場合、 データベースルーター の allow_migrate() メソッドを使うことで、 makemigrations がどのデータベースをチェックし、履歴が一貫しているかを制御できます。
アプリにマイグレーションを追加する¶
新しいアプリはマイグレーションを受け入れるようにあらかじめ設定されているので、ある程度変更したら makemigrations を実行してマイグレーションを追加できます。
もしあなたのアプリがすでにモデルとデータベーステーブルを持っていて、マイグレーションをまだ持っていない場合(たとえば、以前の Django バージョンに対して作成した場合)、下記のコマンドを実行することでマイグレーションを使うように変換する必要があります:
$ python manage.py makemigrations your_app_label
これにより、アプリの新しい初期マイグレーションが作成されます。次に、python manage.py migrate --fake-initial を実行すると、Djangoは初期マイグレーションがあり、かつ 作成しようとするテーブルが既に存在することを検出し、マイグレーションを既に適用済みとしてマークします。 (migrate --fake-initial フラグがない場合、コマンドは作成しようとするテーブルが既に存在するためエラーになります。)
ただし、これは以下の2つの条件を満たした場合にのみ有効です:
テーブルを作ってからモデルを変更していません。マイグレーションを動作させるためには、初期マイグレーションを 最初に 行い、それから変更を加える必要があります。Django は変更をデータベースではなくマイグレーションファイルと比較するからです。
データベースを手動で編集していないので、 Django はデータベースがモデルにマッチしていないことを検知できません。マイグレーションがこれらのテーブルを変更しようとするとエラーが出るだけです。
マイグレーションを元に戻す¶
マイグレーションを元に戻すには、 migrate で前のマイグレーションの番号を渡します。たとえば、マイグレーション books.0003 を元に戻すには、次のようにします:
$ python manage.py migrate books 0002
Operations to perform:
Target specific migration: 0002_auto, from books
Running migrations:
Rendering model states... DONE
Unapplying books.0003_auto... OK
...\> py manage.py migrate books 0002
Operations to perform:
Target specific migration: 0002_auto, from books
Running migrations:
Rendering model states... DONE
Unapplying books.0003_auto... OK
アプリに適用されたマイグレーションをすべて元に戻したい場合は、zero という名前を使います:
$ python manage.py migrate books zero
Operations to perform:
Unapply all migrations: books
Running migrations:
Rendering model states... DONE
Unapplying books.0002_auto... OK
Unapplying books.0001_initial... OK
...\> py manage.py migrate books zero
Operations to perform:
Unapply all migrations: books
Running migrations:
Rendering model states... DONE
Unapplying books.0002_auto... OK
Unapplying books.0001_initial... OK
マイグレーションに不可逆な操作が含まれている場合、そのマイグレーションは不可逆です。このようなマイグレーションを元に戻そうとすると IrreversibleError が発生します:
$ python manage.py migrate books 0002
Operations to perform:
Target specific migration: 0002_auto, from books
Running migrations:
Rendering model states... DONE
Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
...\> py manage.py migrate books 0002
Operations to perform:
Target specific migration: 0002_auto, from books
Running migrations:
Rendering model states... DONE
Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
履歴上のモデル¶
マイグレーションを実行するとき、 Django はマイグレーションファイルに保存されたモデルの過去のバージョンから作業しています。 RunPython オペレーションを使って Python コードを書いたり、データベースルーターに allow_migrate メソッドがある場合、 直接インポートするのではなく、 これらの履歴上のモデルバージョンを 使う必要があります 。
警告
履歴上のモデルを使用するのではなく、モデルを直接インポートした場合、マイグレーションは初期にはうまくいくかもしれませんが、将来、古いマイグレーションを再実行しようとしたときに(通常は新しいインストールをセットアップし、データベースをセットアップするためにすべてのマイグレーションを実行したときに)失敗します。
つまり、履歴上のモデルの問題はすぐには明らかにならない可能性があります。このような不具合が発生した場合は、直接インポートするのではなく、履歴上のモデルを使用するようにマイグレーションを編集し、その変更をコミットすれば問題ありません。
任意のPythonコードをシリアライズすることは不可能なので、これらの履歴上のモデルはあなたが定義したカスタムメソッドを持ちません。しかし、同じフィールド、リレーションシップ、マネージャ(use_in_migrations = True を持つものに限られます)、Meta オプション(バージョン管理されているので、現在のものとは異なるかもしれません)を持つことになります。
警告
つまり、マイグレーションでオブジェクトにアクセスするときに、カスタムメソッド save() が呼び出されることはありませんし、カスタムコンストラクタやカスタムインスタンスメソッドもありません。適切な計画を立ててください!
upload_to や limit_choices_to のようなフィールドオプションの関数への参照や、 use_in_migrations = True を持つマネージャのモデルマネージャ宣言はマイグレーションでシリアライズされます。 カスタムのモデルフィールド もマイグレーションで直接インポートされるので、残しておく必要があります。
加えて、モデルの具体的な基底クラスはポインタとして保存されるので、それらへの参照を含むマイグレーションが存在する限り、常に基底クラスを保持しておく必要があります。 プラス面として、これらの基底クラスからのメソッドとマネージャは普通に継承されるので、どうしてもこれらにアクセスする必要がある場合は、スーパークラスに移動させることを選択できます。
古い参照を削除するには、マイグレーションをスカッシュ(squash) するか、参照数が少ない場合はマイグレーションファイルにコピーします。
モデルのフィールドを削除するときに考えるべきこと¶
前のセクションで説明した「履歴上の機能への参照」の注意事項と同様に、プロジェクトやサードパーティアプリからカスタムモデルフィールドを削除すると、それらが古いマイグレーションで参照されている場合に問題が発生します。
このような状況を解決するために、 Django は システムチェックフレームワーク を使ってモデルフィールドの非推奨化を支援するモデルフィールド属性をいくつか提供しています。
以下のように、system_check_deprecated_details 属性をモデルフィールドに追加します:
class IPAddressField(Field):
system_check_deprecated_details = {
"msg": (
"IPAddressField has been deprecated. Support for it (except "
"in historical migrations) will be removed in Django 1.9."
),
"hint": "Use GenericIPAddressField instead.", # optional
"id": "fields.W900", # pick a unique ID for your field.
}
あなたが選んだ非推奨期間 (Django 自体のフィールドでは 2、3 回の機能リリース) が経過したら、 system_check_deprecated_details 属性を system_check_removed_details に変更し、次のように辞書を更新してください:
class IPAddressField(Field):
system_check_removed_details = {
"msg": (
"IPAddressField has been removed except for support in "
"historical migrations."
),
"hint": "Use GenericIPAddressField instead.",
"id": "fields.E900", # pick a unique ID for your field.
}
__init__()、deconstruct()、get_internal_type() などのデータベースマイグレーションで動作するために必要なフィールドのメソッドは残しておく必要があります。フィールドを参照するマイグレーションが存在する限り、このスタブフィールドを保持します。たとえば、マイグレーションを潰して(squashして)古いマイグレーションを削除したら、フィールドを完全に削除できるはずです。
データのマイグレーション¶
データベースのスキーマを変更するだけでなく、必要であれば、マイグレーションを使用して、スキーマと連動してデータベース自体のデータを変更することもできます。
データを変更するマイグレーションは通常「データマイグレーション」と呼びます。スキーママイグレーションと並行して、別のマイグレーションとして記述するのが最適です。
Django はスキーママイグレーションのようにデータマイグレーションを自動生成することはできませんが、書くのはそれほど難しくありません。Django のマイグレーションファイルは オペレーション で構成され、データマイグレーションに使う主なオペレーションは RunPython です。
手始めに、作業可能な空のマイグレーションファイルを作成してください (Django がファイルを適切な場所に置き、名前を提案し、依存関係を追加してくれます):
python manage.py makemigrations --empty yourappname
そして、そのファイルを開いてください。次のようなファイルになっているはずです:
# Generated by Django A.B on YYYY-MM-DD HH:MM
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("yourappname", "0001_initial"),
]
operations = []
あとは新しい関数を作って RunPython に使ってもらうだけです。 RunPython は、引数として2つの引数を取る呼び出し可能オブジェクトを期待します。1つ目は アプリ レジストリ で、マイグレーションが履歴のどこに位置するかに合わせて、履歴上のモデルのバージョンを読み込んだものです。そしてもう1つは スキーマエディタ で、手動でデータベースのスキーマを変更するのに使用できます (ただし、これをやるとマイグレーション自動検出器が混乱してしまうので注意してください!)。
新しい name フィールドに first_name と last_name を組み合わせた値を入力するマイグレーションを書いてみましょう (すべての人が姓と名を持つわけではないことに気がつきました)。あとは履歴上のモデルを使用して行をイテレートするだけです:
from django.db import migrations
def combine_names(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Person = apps.get_model("yourappname", "Person")
for person in Person.objects.all():
person.name = f"{person.first_name} {person.last_name}"
person.save()
class Migration(migrations.Migration):
dependencies = [
("yourappname", "0001_initial"),
]
operations = [
migrations.RunPython(combine_names),
]
これが完了したら、通常通り python manage.py migrate を実行し、他のマイグレーションと並行してデータマイグレーションを実行します。
2 番目の呼び出し可能オブジェクトを RunPython に渡すことで、逆方向へのマイグレーションの際に実行したいロジックを実行できます。この呼び出し可能オブジェクトが省略された場合、逆方向にマイグレーションすると例外が発生します。
他のアプリからモデルにアクセスする¶
RunPython 関数で、マイグレーションが配置されているアプリ以外のアプリのモデルを使用する場合、マイグレーションの dependencies 属性には、関係する各アプリの最新のマイグレーションが含まれている必要があります。含まれていない場合、 RunPython 関数内で apps.get_model() を使用してモデルを取得しようとすると、 LookupError: No installed app with label 'myappname' のようなエラーが発生する可能性があります。
次の例では、app1 のマイグレーションで、app2 のモデルを使用する必要があります。私たちは move_m1 が両方のアプリのモデルにアクセスする必要があるという事実以外、その詳細には関心がありません。そのため、app2 の最後のマイグレーションを指定する依存関係を追加しました:
class Migration(migrations.Migration):
dependencies = [
("app1", "0001_initial"),
# added dependency to enable using models from app2 in move_m1
("app2", "0004_foobar"),
]
operations = [
migrations.RunPython(move_m1),
]
より高度なマイグレーション¶
より高度なマイグレーション操作に興味があったり、自分でマイグレーションを書けるようになりたければ、 マイグレーション操作のリファレンス と マイグレーションを書く の "how-to" を参照してください。
マイグレーションのスカッシュ (Squash)¶
マイグレーションは自由に作成し、その数について心配する必要はありません。マイグレーションコードは、一度に数百ものマイグレーションを効率的に扱えるよう最適化されています。しかし、最終的には数百ものマイグレーションを数個にまとめたいと思う時が来ます。その時には、マイグレーションの圧縮 (squash) が役立ちます。
スカッシュ (squash: 潰す) とは、既存の多数のマイグレーションから、同じ変更を表すマイグレーションを1つ (場合によっては数個) に減らすことです。
Django は、既存のマイグレーションをすべて取り出し、その Operation を抽出して順番に並べ、オプティマイザを実行してリストの長さを短くしようとします。 例えば、 CreateModel と DeleteModel が互いに打ち消し合うことも、 AddField が CreateModel にロールインできることも知っています。
操作シーケンスを可能な限り減らしたら (どれだけ減らせるかは、モデルがどれだけ密接に絡み合っているか、また elidable とマークされていない限り最適化できない RunSQL や RunPython 操作があるかどうかによって決まります)、Django はそれを新しいマイグレーションファイル群に書き戻します。
これらのファイルは、以前にスカッシュされたマイグレーションを置き換えるものとしてマークされているため、古いマイグレーションファイルと共存でき、Djangoは履歴のどこにいるかに応じて賢く切り替えます。スカッシュしたマイグレーションセットの途中であれば、それらを使用し続け、最後に達した後でスカッシュされた履歴に切り替えます。一方、新しいインストールでは新しいスカッシュマイグレーションが使用され、古いものはすべてスキップされます。
これにより、まだ完全に最新の状態ではない本番システムを混乱させずにマイグレーションを圧縮できます。推奨されるプロセスは、古いファイルを保持しながらスカッシュし、コミットしてリリースし、すべてのシステムが新しいリリースでアップグレードされるのを待つことです (または、サードパーティのプロジェクトであれば、ユーザーがリリースを順番にアップグレードするようにします)。その後、古いファイルを削除し、コミットして2回目のリリースを行います。
このプロセスを支えるコマンドは squashmigrations です。スカッシュしたいアプリのラベルとマイグレーション名を指定して実行すると、作業が始まります。
$ ./manage.py squashmigrations myapp 0004
Will squash the following migrations:
- 0001_initial
- 0002_some_change
- 0003_another_change
- 0004_undo_something
Do you wish to proceed? [y/N] y
Optimizing...
Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_something.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.
squashmigrations --squashed-name オプションを使用すると、自動生成された名前を使用する代わりに、スカッシュされたマイグレーションの名前を指定できます。
Djangoのモデル間の依存関係は非常に複雑になる可能性があり、マイグレーションのスカッシュによっては正常に実行されないマイグレーションが発生することがあります。これには、最適化されていない場合 (この場合は --no-optimize オプションを試すか、問題を報告してください) や、CircularDependencyError が発生した場合が含まれます。後者の場合は、手動で解決する必要があります。
CircularDependencyError を手動で解決するには、循環依存ループ内の外部キーの1つを別のマイグレーションに分割し、その他のアプリへの依存をそれと一緒に移動します。不明な場合は、モデルから新しいマイグレーションを作成するように依頼されたときに makemigrations が問題をどのように扱うかを確認してください。将来のDjangoリリースでは、 squashmigrations がこれらのエラーを自動的に解決しようとする機能が追加される予定です。
マイグレーションをスカッシュしたら、それを置き換えるマイグレーションと共にコミットし、アプリケーションを実行しているすべてのインスタンスにこの変更を配布し、データベースに変更を記録するために migrate を確実に実行してください。
その後、スカッシュしたマイグレーションを通常のマイグレーションに移行するには、次の手順を実行する必要があります:
マイグレーションが置き換えるすべてのマイグレーションファイルを削除します。
削除されたマイグレーションに依存しているすべてのマイグレーションを、 代わりにスカッシュしたマイグレーションに依存するように更新します。
スカッシュしたマイグレーションの
Migrationクラスのreplaces属性を削除します (これで Django はそれがスカッシュしたマイグレーションだとわかります)。
注釈
一度マイグレーションをスカッシュしたら、通常のマイグレーションに完全に移行するまで、そのスカッシュしたマイグレーションを再度スカッシュすべきではありません。
削除されたマイグレーションへの参照の整理
削除したマイグレーションの名前を将来再利用する可能性がある場合は、 migrate --prune オプションを使って Django の migrations テーブルからそのマイグレーションへの参照を削除してください。
値のシリアライズ¶
マイグレーションは、モデルの古い定義が書かれた Python ファイルです。よって、マイグレーションを書くには、Django はモデルの現在の状態を取得して、その状態をシリアライズしてファイルに出力できなければなりません。
Django はほとんどのオブジェクトをシリアライズできますが、有効な Python 表現へと単純にはシリアライズできないオブジェクトもあります。しかし、任意の値を Python のコードにデコードするような Python の標準は存在しません(repr() は基本的な値にしか機能しませんし、import path は指定できません)。
Django がシリアライズできるのは、以下のオブジェクトです。
int、float、bool、str、bytes、None、NoneTypelist、set、tuple、dict、range。datetime.date、datetime.time、datetime.datetimeインスタンス (timezone-aware なものも含む)decimal.Decimalインスタンスenum.Enumまたはenum.Flagインスタンスuuid.UUIDインスタンスシリアライズできる
func、args、keywordsの値を持つfunctools.partial()またはfunctools.partialmethodインスタンスpathlibから取得した純粋パスオブジェクトと具象パスオブジェクト。たとえば、pathlib.PosixPathはpathlib.PurePosixPathに変換されます。os.PathLikeインスタンス、たとえばos.DirEntryは、os.fspath()を使用してstrまたはbytesに変換されます。シリアライズできる値をラッピングした
LazyObjectインスタンス列挙型 (
TextChoicesやIntegerChoicesなど) のインスタンス。任意の Django フィールド
任意の関数またはメソッドへのリファレンス (例:
datetime.datetime.today) (ただし、モジュールのトップレベルのスコープにいなければならない)関数は適切にラップされていればデコレートできます、つまり
functools.wraps()を使います。functools.cache()デコレータとfunctools.lru_cache()デコレータは明示的にサポートされています。
クラスの本体から使用されている束縛されていないメソッド
任意のクラスへのリファレンス (ただし、モジュールのトップレベルのスコープにいなければならない)
カスタムの
deconstruct()メソッドを持つすべてのオブジェクト (以下を参照)
functools.cache() または functools.lru_cache() で装飾された関数のシリアライズのサポートが追加されました。
Django は以下のオブジェクトをシリアライズできません。
ネストしたクラス
任意のクラスのインスタンス (例:
MyClass(4.3, 5.7))ラムダ式
カスタムシリアライザ¶
カスタムシリアライザを書けば、他の型もシリアライズできます。たとえば、 Django がデフォルトで Decimal をシリアライズしていない場合、次のようにできます:
from decimal import Decimal
from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter
class DecimalSerializer(BaseSerializer):
def serialize(self):
return repr(self.value), {"from decimal import Decimal"}
MigrationWriter.register_serializer(Decimal, DecimalSerializer)
MigrationWriter.register_serializer() の第一引数には、シリアライザを使用する型または型のイテラブルを指定します。
シリアライザの serialize() メソッドは、値がマイグレーションでどのように表示されるかを示す文字列と、マイグレーションで必要なインポートのセットを返す必要があります。
deconstruct() メソッドを追加する¶
自作のカスタムクラスに deconstruct() メソッドを実装することで、インスタンスを Django にシリアライズさせることができます。このメソッドは引数を取らず、 (path, args, kwargs) からなる3タプルを返す必要があります。
pathはクラス名を最後に含むクラスへの Python パスでなければなりません (たとえば、myapp.custom_things.MyClass)。自作のクラスがモジュールのトップレベルで使用できない場合は、シリアライズすることはできません。argsはクラスの__init__メソッドに渡される位置引数のリストでなければなりません。このリストに含まれる要素は、それ自体でシリアライズ可能である必要があります。kwargsはクラスの__init__メソッドに渡されるキーワード引数の dict でなければなりません。すべての値はそれ自体でシリアライズ可能である必要があります。
注釈
この戻り値は カスタムフィールドのための deconstruct() メソッド とは異なり、4つの項目のタプルを返します。
Django は、Django のフィールドへの参照を書き出すのと同じように、与えられた引数を持つクラスのインスタンス化として値を書き出します。
makemigrations が実行されるたびに新しいマイグレーションが作成されるのを防ぐために、クラスに追加情報を与える __eq__() メソッドも追加した方がいいでしょう。この関数は、Django のマイグレーションフレームワークが状態の変更を検出するために呼び出します。
クラスのコンストラクタのすべての引数がそれ自体でシリアライズ可能である場合には、次のように django.utils.deconstruct の @deconstructible クラスデコレータを使うことで deconstruct() メソッドを追加できます。
from django.utils.deconstruct import deconstructible
@deconstructible
class MyCustomClass:
def __init__(self, foo=1):
self.foo = foo
...
def __eq__(self, other):
return self.foo == other.foo
デコレータは、コンストラクタに与えられる引数を独自の方法でキャプチャし保存しておきます。そして、deconstruct() が呼ばれるタイミングで、保存しておいた引数を返すようにしてくれます。
Django の複数のバージョンをサポートする¶
もしあなたが、モデルを持つサードパーティのアプリのメンテナならば、Django の複数のバージョンをサポートするマイグレーションを入れておきたいでしょう。その場合、必ず あなたがサポートしたい Django の下限のバージョンで makemigrations を実行するようにしてください。
マイグレーションのシステムは、Django の他のポリシーと同じく、後方互換性を持ちます。そのため、Django X.Y で生成されたマイグレーションファイルは、変更なしに Django X.Y+1 で動作します。しかし、マイグレーションのシステムは前方互換性は保証しません。新しい機能が追加され、新しいバージョンの Django でマイグレーションファイルが生成されれば、そのマイグレーションは古いバージョンでは動きません。
参考
- マイグレーション操作リファレンス
スキーマ操作の API、特別な操作、自作の操作の書き方などについて書いてあります。
- マイグレーション (Migrations) を書くための ”how-to”
遭遇するかもしれない異なるシチュエーション下での、データベースのマイグレーションの構造化方法と書き方を説明しています。