データベースの移行を管理することは、どのソフトウェアプロジェクトでも大きな課題です。幸い、バージョン1.7の時点で、Djangoには組み込みの移行フレームワークが付属しています。このフレームワークは非常に強力で、データベースの変更を管理するのに役立ちます。しかし、フレームワークによって提供される柔軟性には、いくつかの妥協が必要でした。 Djangoの移行の制限を理解するために、ダウンタイムなしでDjangoにインデックスを作成するというよく知られた問題に取り組みます。
このチュートリアルでは、次のことを学びます。
- Djangoが新しい移行を生成する方法とタイミング
- 移行を実行するためにDjangoが生成するコマンドを検査する方法
- ニーズに合わせて移行を安全に変更する方法
この中級レベルのチュートリアルは、Djangoの移行に既に精通している読者を対象としています。そのトピックの概要については、Django Migrations:APrimerをご覧ください。
無料ボーナス: ここをクリックして、PythonWeb開発スキルを深めるために使用できる追加のDjangoチュートリアルとリソースに無料でアクセスできます。
Djangoの移行でインデックスを作成する際の問題
アプリケーションによって保存されるデータが大きくなるときに通常必要になる一般的な変更は、インデックスの追加です。インデックスは、クエリを高速化し、アプリを高速で応答性の高いものにするために使用されます。
ほとんどのデータベースでは、インデックスを追加するには、テーブルの排他ロックが必要です。排他的ロックは、UPDATE
などのデータ変更(DML)操作を防ぎます 、INSERT
、およびDELETE
、インデックスの作成中。
ロックは、特定の操作を実行するときにデータベースによって暗黙的に取得されます。たとえば、ユーザーがアプリにログインすると、Djangoはlast_login
を更新します auth_user
のフィールド テーブル。更新を実行するには、データベースは最初に行のロックを取得する必要があります。行が現在別の接続によってロックされている場合は、データベース例外が発生する可能性があります。
移行中にシステムを使用可能な状態に保つ必要がある場合、テーブルをロックすると問題が発生する可能性があります。テーブルが大きいほど、インデックスの作成に時間がかかる可能性があります。インデックスの作成に時間がかかるほど、システムが使用できなくなったり、ユーザーに応答しなくなったりする時間が長くなります。
一部のデータベースベンダーは、テーブルをロックせずにインデックスを作成する方法を提供しています。たとえば、テーブルをロックせずにPostgreSQLでインデックスを作成するには、CONCURRENTLY
を使用できます。 キーワード:
CREATE INDEX CONCURRENTLY ix ON table (column);
Oracleには、ONLINE
があります。 インデックスの作成中にテーブルでDML操作を許可するオプション:
CREATE INDEX ix ON table (column) ONLINE;
移行を生成するとき、Djangoはこれらの特別なキーワードを使用しません。移行をそのまま実行すると、データベースはテーブルの排他ロックを取得し、インデックスの作成中にDML操作を防止します。
インデックスを同時に作成することにはいくつかの注意点があります。データベースバックエンドに固有の問題を事前に理解しておくことが重要です。たとえば、PostgreSQLの注意点の1つは、追加のテーブルスキャンが必要になるため、インデックスの作成に時間がかかることです。
このチュートリアルでは、Djangoの移行を使用して、ダウンタイムを発生させることなく、大きなテーブルにインデックスを作成します。
注: このチュートリアルに従うには、PostgreSQLバックエンド、Django 2.x、およびPython3を使用することをお勧めします。
他のデータベースバックエンドと一緒にフォローすることも可能です。 PostgreSQLに固有のSQL機能が使用されている場所では、データベースのバックエンドに一致するようにSQLを変更します。
セットアップ
構成されたSale
を使用します app
というアプリのモデル 。実際の状況では、Sale
などのモデル はデータベースのメインテーブルであり、通常は非常に大きく、大量のデータを格納します。
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
)
charged_amount = models.PositiveIntegerField()
テーブルを作成するには、最初の移行を生成して適用します。
$ python manage.py makemigrations
Migrations for 'app':
app/migrations/0001_initial.py
- Create model Sale
$ python manage migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0001_initial... OK
しばらくすると、販売テーブルが非常に大きくなり、ユーザーは遅いと不満を言うようになります。データベースを監視しているときに、多くのクエリがsold_at
を使用していることに気付きました。 桁。処理を高速化するには、列にインデックスが必要であると判断します。
sold_at
にインデックスを追加するには 、モデルに次の変更を加えます。
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
)
charged_amount = models.PositiveIntegerField()
この移行をそのまま実行すると、Djangoはテーブルにインデックスを作成し、インデックスが完了するまでロックされます。非常に大きなテーブルにインデックスを作成するには時間がかかる場合があり、ダウンタイムを回避する必要があります。
データセットが小さく、接続が非常に少ないローカル開発環境では、この移行は瞬時に感じられる場合があります。ただし、多数の同時接続がある大規模なデータセットでは、ロックの取得とインデックスの作成に時間がかかる場合があります。
次の手順では、Djangoによって作成された移行を変更して、ダウンタイムを発生させずにインデックスを作成します。
偽の移行
最初のアプローチは、インデックスを手動で作成することです。移行を生成しますが、実際にDjangoに適用させることはしません。代わりに、データベースでSQLを手動で実行してから、Djangoに移行が完了したと思わせるようにします。
まず、移行を生成します:
$ python manage.py makemigrations --name add_index_fake
Migrations for 'app':
app/migrations/0002_add_index_fake.py
- Alter field sold_at on sale
sqlmigrate
を使用します この移行を実行するためにDjangoが使用するSQLを表示するコマンド:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
テーブルをロックせずにインデックスを作成したいので、コマンドを変更する必要があります。 CONCURRENTLY
を追加します キーワードを設定し、データベースで実行します:
app=# CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
CREATE INDEX
BEGIN
なしでコマンドを実行したことに注意してください およびCOMMIT
部品。これらのキーワードを省略すると、データベーストランザクションなしでコマンドが実行されます。データベーストランザクションについては、この記事の後半で説明します。
コマンドを実行した後、移行を適用しようとすると、次のエラーが発生します。
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake...Traceback (most recent call last):
File "venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
psycopg2.ProgrammingError: relation "app_sale_sold_at_b9438ae4" already exists
Djangoはインデックスがすでに存在するため、移行を続行できないと文句を言います。データベースに直接インデックスを作成したので、今度はDjangoに移行がすでに適用されていると思わせる必要があります。
移行を偽造する方法
Djangoは、移行を実際に実行せずに、移行を実行済みとしてマークする組み込みの方法を提供します。このオプションを使用するには、--fake
を設定します 移行を適用する際のフラグ:
$ python manage.py migrate --fake
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake... FAKED
今回、Djangoはエラーを発生させませんでした。実際、Djangoは実際には移行を適用しませんでした。実行済み(またはFAKED
)としてマークしただけです 。
移行を偽造する際に考慮すべきいくつかの問題があります:
-
手動コマンドは、Djangoによって生成されたSQLと同等である必要があります: 実行するコマンドがDjangoによって生成されたSQLと同等であることを確認する必要があります。
sqlmigrate
を使用します SQLコマンドを生成します。コマンドが一致しない場合、データベースとモデルの状態の間に不整合が生じる可能性があります。 -
その他の適用されていない移行も偽造されます: 適用されていない移行が複数ある場合、それらはすべて偽造されます。移行を適用する前に、偽造したい移行のみが適用されていないことを確認することが重要です。そうしないと、不整合が発生する可能性があります。もう1つのオプションは、偽造する正確な移行を指定することです。
-
データベースへの直接アクセスが必要です: データベースでSQLコマンドを実行する必要があります。これは常にオプションではありません。また、本番データベースでコマンドを直接実行することは危険であり、可能な場合は避ける必要があります。
-
自動導入プロセスには調整が必要な場合があります: 展開プロセスを自動化した場合(CI、CD、またはその他の自動化ツールを使用)、プロセスを変更して偽の移行を行う必要がある場合があります。これは必ずしも望ましいことではありません。
クリーンアップ
次のセクションに進む前に、最初の移行直後にデータベースを元の状態に戻す必要があります。これを行うには、最初の移行に戻ります:
$ python manage.py migrate 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_fake... OK
Djangoは2回目の移行で加えられた変更を適用しなかったため、ファイルも安全に削除できるようになりました:
$ rm app/migrations/0002_add_index_fake.py
すべてが正しく行われたことを確認するには、移行を調べます。
$ python manage.py showmigrations app
app
[X] 0001_initial
最初の移行が適用され、適用されていない移行はありません。
移行で生のSQLを実行する
前のセクションでは、データベースでSQLを直接実行し、移行を偽造しました。これで作業は完了しますが、より良い解決策があります。
Djangoは、RunSQL
を使用して移行で生のSQLを実行する方法を提供します 。データベースで直接コマンドを実行する代わりに、それを使用してみましょう。
まず、新しい空の移行を生成します:
$ python manage.py makemigrations app --empty --name add_index_runsql
Migrations for 'app':
app/migrations/0002_add_index_runsql.py
次に、移行ファイルを編集して、RunSQL
を追加します 操作:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
),
]
移行を実行すると、次の出力が得られます。
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_runsql... OK
これは見栄えが良いですが、問題があります。もう一度移行を生成してみましょう:
$ python manage.py makemigrations --name leftover_migration
Migrations for 'app':
app/migrations/0003_leftover_migration.py
- Alter field sold_at on sale
Djangoは同じ移行を再度生成しました。なぜそれをしたのですか?
クリーンアップ
その質問に答える前に、データベースに加えた変更をクリーンアップして元に戻す必要があります。最後の移行を削除することから始めます。適用されなかったため、削除しても安全です:
$ rm app/migrations/0003_leftover_migration.py
次に、app
の移行を一覧表示します アプリ:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
3番目の移行はなくなりましたが、2番目の移行が適用されます。最初の移行直後の状態に戻したいと考えています。前のセクションで行ったように、最初の移行に戻してみてください:
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql...Traceback (most recent call last):
NotImplementedError: You cannot reverse this operation
Djangoは移行を元に戻すことができません。
逆移行操作
移行を元に戻すために、Djangoはすべての操作に対して反対のアクションを実行します。この場合、インデックスを追加するのとは逆に、インデックスを削除します。すでに見てきたように、移行が元に戻せる場合は、適用を解除できます。 checkout
を使用できるのと同じように Gitでは、migrate
を実行すると、移行を元に戻すことができます 以前の移行へ。
多くの組み込みの移行操作では、すでに逆のアクションが定義されています。たとえば、フィールドを追加するための逆のアクションは、対応する列を削除することです。モデルを作成するための逆のアクションは、対応するテーブルを削除することです。
一部の移行操作は元に戻せません。たとえば、移行が適用されるとデータが失われるため、フィールドを削除したりモデルを削除したりするための逆のアクションはありません。
前のセクションでは、RunSQL
を使用しました 手術。移行を元に戻そうとすると、エラーが発生しました。エラーによると、移行中の操作の1つを元に戻すことはできません。 Djangoはデフォルトで生のSQLを逆にすることができません。 Djangoは操作によって何が実行されたかを認識していないため、反対のアクションを自動的に生成することはできません。
移行をリバーシブルにする方法
移行を元に戻すには、移行内のすべての操作を元に戻す必要があります。移行の一部を元に戻すことはできないため、1回の元に戻せない操作で、移行全体を元に戻すことはできません。
RunSQL
を作成するには 操作を元に戻すことができる場合は、操作を元に戻したときに実行するSQLを提供する必要があります。リバースSQLはreverse_sql
で提供されます 引数。
インデックスを追加するのとは逆のアクションは、インデックスを削除することです。移行を元に戻すには、reverse_sql
を指定します インデックスを削除するには:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
reverse_sql='DROP INDEX "app_sale_sold_at_b9438ae4";',
),
]
次に、移行を元に戻してみてください:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql... OK
$ python manage.py showmigrations app
app
[X] 0001_initial
[ ] 0002_add_index_runsql
2回目の移行が取り消され、Djangoによってインデックスが削除されました。これで、移行ファイルを安全に削除できます:
$ rm app/migrations/0002_add_index_runsql.py
reverse_sql
を提供することは常に良い考えです 。生のSQL操作を元に戻すのにアクションが必要ない状況では、特別な番兵migrations.RunSQL.noop
を使用して、操作を元に戻すことができるとマークできます。 :
migrations.RunSQL(
sql='...', # Your forward SQL here
reverse_sql=migrations.RunSQL.noop,
),
モデルの状態とデータベースの状態を理解する
RunSQL
を使用して手動でインデックスを作成する以前の試み 、データベースにインデックスが作成されていても、Djangoは同じ移行を何度も生成しました。 Djangoがそれを行った理由を理解するには、まずDjangoが新しい移行をいつ生成するかを決定する方法を理解する必要があります。
Djangoが新しい移行を生成するとき
移行を生成して適用するプロセスで、Djangoはデータベースの状態とモデルの状態を同期します。たとえば、モデルにフィールドを追加すると、Djangoはテーブルに列を追加します。モデルからフィールドを削除すると、Djangoはテーブルから列を削除します。
モデルとデータベースを同期するために、Djangoはモデルを表す状態を維持します。データベースをモデルと同期するために、Djangoは移行操作を生成します。移行操作は、データベースで実行できるベンダー固有のSQLに変換されます。すべての移行操作が実行されると、データベースとモデルは一貫していることが期待されます。
データベースの状態を取得するために、Djangoは過去のすべての移行からの操作を集約します。移行の集約された状態がモデルの状態と一致しない場合、Djangoは新しい移行を生成します。
前の例では、生のSQLを使用してインデックスを作成しました。 Djangoは、使い慣れた移行操作を使用していなかったため、インデックスを作成したことを知りませんでした。
Djangoがすべての移行を集約し、それらをモデルの状態と比較したところ、インデックスが欠落していることがわかりました。これが、インデックスを手動で作成した後でも、Djangoがインデックスが欠落していると考え、新しい移行を生成した理由です。
移行時にデータベースと状態を分離する方法
Djangoは希望どおりにインデックスを作成できないため、独自のSQLを提供する必要がありますが、それでもDjangoに作成したことを通知します。
つまり、データベースで何かを実行し、Djangoに内部状態を同期するための移行操作を提供する必要があります。そのために、DjangoはSeparateDatabaseAndState
と呼ばれる特別な移行操作を提供します 。この操作はよく知られていないため、このような特別な場合のために予約する必要があります。
移行を最初から作成するよりも編集する方がはるかに簡単なので、通常の方法で移行を生成することから始めます。
$ python manage.py makemigrations --name add_index_separate_database_and_state
Migrations for 'app':
app/migrations/0002_add_index_separate_database_and_state.py
- Alter field sold_at on sale
これは、以前と同じように、Djangoによって生成された移行の内容です:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
]
DjangoはAlterField
を生成しました フィールドsold_at
に対する操作 。この操作により、インデックスが作成され、状態が更新されます。この操作を維持したいのですが、データベースで実行する別のコマンドを提供します。
もう一度、コマンドを取得するには、Djangoによって生成されたSQLを使用します:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
CONCURRENTLY
を追加します 適切な場所のキーワード:
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
次に、移行ファイルを編集し、SeparateDatabaseAndState
を使用します 変更したSQLコマンドを実行用に提供するには:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""", reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
移行操作SeparateDatabaseAndState
2つの操作リストを受け入れます:
- state_operations 内部モデルの状態に適用する操作です。データベースには影響しません。
- database_operations データベースに適用する操作です。
Djangoによって生成された元の操作をstate_operations
に保持しました 。 SeparateDatabaseAndState
を使用する場合 、これはあなたが通常やりたいことです。 db_index=True
に注意してください フィールドに引数が提供されます。この移行操作により、フィールドにインデックスがあることがDjangoに通知されます。
Djangoによって生成されたSQLを使用し、CONCURRENTLY
を追加しました キーワード。特別なアクションRunSQL
を使用しました 移行時に生のSQLを実行します。
移行を実行しようとすると、次の出力が表示されます。
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state...Traceback (most recent call last):
File "/venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 83, in _execute
return self.cursor.execute(sql)
psycopg2.InternalError: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
非原子移動
SQLでは、CREATE
、DROP
、ALTER
、およびTRUNCATE
操作はデータ定義言語と呼ばれます (DDL)。 PostgreSQLなどのトランザクションDDLをサポートするデータベースでは、Djangoはデフォルトでデータベーストランザクション内で移行を実行します。ただし、上記のエラーによると、PostgreSQLはトランザクションブロック内に同時にインデックスを作成することはできません。
移行内で同時にインデックスを作成できるようにするには、データベーストランザクションで移行を実行しないようにDjangoに指示する必要があります。これを行うには、atomic
を設定して、移行を非アトミックとしてマークします。 False
へ :
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""",
reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
移行を非アトミックとしてマークした後、移行を実行できます:
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state... OK
ダウンタイムを発生させることなく、移行を実行しただけです。
SeparateDatabaseAndState
を使用する際に考慮すべきいくつかの問題があります :
-
データベース操作は状態操作と同等である必要があります: データベースとモデルの状態の不一致は、多くの問題を引き起こす可能性があります。良い出発点は、Djangoによって生成された操作を
state_operations
に保持することです。sqlmigrate
の出力を編集しますdatabase_operations
で使用する 。 -
エラーが発生した場合、非アトミック移行はロールバックできません: 移行中にエラーが発生した場合、ロールバックすることはできません。移行をロールバックするか、手動で完了する必要があります。非アトミックマイグレーション内で実行される操作を最小限に抑えることをお勧めします。移行に追加の操作がある場合は、それらを新しい移行に移動します。
-
移行はベンダー固有の場合があります: Djangoによって生成されるSQLは、プロジェクトで使用されるデータベースバックエンドに固有のものです。他のデータベースバックエンドで動作する可能性がありますが、それは保証されていません。複数のデータベースバックエンドをサポートする必要がある場合は、このアプローチにいくつかの調整を加える必要があります。
結論
このチュートリアルは、大きなテーブルと問題から始めました。ユーザーにとってアプリをより高速にしたいと考えていました。また、ダウンタイムを発生させずにそれを実現したいと考えていました。
チュートリアルの終わりまでに、この目標を達成するためにDjango移行を生成して安全に変更することができました。途中でさまざまな問題に取り組み、移行フレームワークが提供する組み込みツールを使用してそれらを克服することができました。
このチュートリアルでは、次のことを学びました。
- モデルとデータベースの状態を使用してDjangoの移行が内部的にどのように機能するか、および新しい移行がいつ生成されるか
-
RunSQL
を使用して移行でカスタムSQLを実行する方法 アクション - リバーシブル移行とは何か、および
RunSQL
を作成する方法 アクションリバーシブル - アトミックマイグレーションとは何か、およびニーズに応じてデフォルトの動作を変更する方法
- Djangoで複雑な移行を安全に実行する方法
モデルとデータベースの状態を分離することは重要な概念です。それとその利用方法を理解すると、組み込みの移行操作の多くの制限を克服できます。頭に浮かぶいくつかのユースケースには、データベースにすでに作成されているインデックスの追加や、DDLコマンドへのベンダー固有の引数の提供が含まれます。