Управлението на миграцията на бази данни е голямо предизвикателство във всеки софтуерен проект. За щастие, от версия 1.7, Django идва с вградена рамка за миграция. Рамката е много мощна и полезна при управлението на промените в базите данни. Но гъвкавостта, осигурена от рамката, изискваше известни компромиси. За да разберете ограниченията на миграцията на Django, ще се заемете с добре познат проблем:създаване на индекс в Django без прекъсване.
В този урок ще научите:
- Как и кога Django генерира нови миграции
- Как да проверявате командите, които Django генерира за изпълнение на миграции
- Как безопасно да променяте миграциите, за да отговарят на вашите нужди
Този урок на средно ниво е предназначен за читатели, които вече са запознати с миграциите на Django. За въведение в тази тема вижте Django Migrations:A Primer.
Безплатен бонус: Щракнете тук, за да получите безплатен достъп до допълнителни уроци и ресурси за Django, които можете да използвате, за да задълбочите своите умения за уеб разработка на Python.
Проблемът със създаването на индекс в Django Migrations
Често срещана промяна, която обикновено става необходима, когато данните, съхранявани от вашето приложение, нарастват, е добавянето на индекс. Индексите се използват, за да ускорят заявките и да направят приложението ви бързо и отзивчиво.
В повечето бази данни добавянето на индекс изисква изключително заключване на таблицата. Изключителното заключване предотвратява операции за промяна на данни (DML), като UPDATE
, 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 е, че създаването на индекс едновременно отнема повече време, защото изисква допълнително сканиране на таблицата.
В този урок ще използвате миграции на Django, за да създадете индекс на голяма таблица, без да причинявате престой.
Забележка: За да следвате този урок, се препоръчва да използвате PostgreSQL backend, Django 2.x и Python 3.
Възможно е да се следват заедно с други бекендове на база данни. На места, където се използват функции на SQL, уникални за PostgreSQL, променете 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
команда за преглед на SQL, който Django ще използва, за да изпълни тази миграция:
$ 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
).
Ето някои въпроси, които трябва да имате предвид при фалшифициране на миграции:
-
Ръчната команда трябва да е еквивалентна на SQL, генериран от Django: Трябва да се уверите, че командата, която изпълнявате, е еквивалентна на SQL, генериран от Django. Използвайте
sqlmigrate
за да произведете SQL командата. Ако командите не съвпадат, тогава може да се стигне до несъответствия между базата данни и състоянието на моделите. -
Други неприложени миграции също ще бъдат фалшифицирани: Когато имате множество неприложени миграции, всички те ще бъдат фалшифицирани. Преди да приложите миграции, важно е да се уверите, че само миграциите, които искате да фалшифицирате, не са приложени. В противен случай може да се стигне до несъответствия. Друга възможност е да посочите точната миграция, която искате да фалшифицирате.
-
Изисква се директен достъп до базата данни: Трябва да изпълните 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 не приложи промените, направени при втората миграция, така че сега е безопасно и да изтриете файла:
$ rm app/migrations/0002_add_index_fake.py
За да се уверите, че сте направили всичко правилно, проверете миграциите:
$ python manage.py showmigrations app
app
[X] 0001_initial
Първоначалната миграция беше приложена и няма неприложени миграции.
Изпълнете необработен SQL в миграции
В предишния раздел изпълнихте SQL директно в базата данни и фалшифицирахте миграцията. Това свършва работата, но има по-добро решение.
Django предоставя начин за изпълнение на необработен SQL при миграции с помощта на RunSQL
. Нека се опитаме да го използваме, вместо да изпълняваме командата директно в базата данни.
Първо генерирайте нова празна миграция:
$ 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
Третата миграция е изчезнала, но втората е приложена. Искате да се върнете в състоянието веднага след първоначалната миграция. Опитайте се да мигрирате обратно към първоначалната миграция, както направихте в предишния раздел:
$ 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
операция. Когато се опитахте да обърнете миграцията, срещнахте грешка. Според грешката една от операциите в миграцията не може да бъде отменена. Django не може да обърне необработения SQL по подразбиране. Тъй като Django не знае какво е било изпълнено от операцията, то не може да генерира обратно действие автоматично.
Как да направите миграцията обратима
За да бъде една миграция обратима, всички операции в нея трябва да бъдат обратими. Не е възможно да се обърне част от миграцията, така че една необратима операция ще направи цялата миграция необратима.
За да направите 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
Втората миграция беше обърната и индексът беше премахнат от 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
. Операцията ще създаде индекс и ще актуализира състоянието. Искаме да запазим тази операция, но да предоставим друга команда за изпълнение в базата данни.
Още веднъж, за да получите командата, използвайте SQL, генериран от Django:
$ 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 списъка с операции:
- операции_състояние са операции, които се прилагат към състоянието на вътрешния модел. Те не оказват влияние върху базата данни.
- операции_база_данни са операции за прилагане към базата данни.
Запазихте оригиналната операция, генерирана от Django, в state_operations
. Когато използвате SeparateDatabaseAndState
, това е, което обикновено искате да направите. Забележете, че db_index=True
аргументът се предоставя на полето. Тази операция по мигриране ще уведоми Django, че има индекс в полето.
Използвахте SQL, генериран от Django, и добавихте 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). В бази данни, които поддържат транзакционен DDL, като PostgreSQL, 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
. -
Неатомните миграции не могат да се върнат назад в случай на грешка: Ако има грешка по време на миграцията, тогава няма да можете да връщате назад. Ще трябва или да отмените миграцията, или да я завършите ръчно. Добра идея е операциите, изпълнявани в рамките на неатомна миграция, да се сведат до минимум. Ако имате допълнителни операции в миграцията, преместете ги в нова миграция.
-
Миграцията може да е специфична за доставчика: SQL, генериран от Django, е специфичен за бекенда на базата данни, използван в проекта. Може да работи с други бекендове на база данни, но това не е гарантирано. Ако трябва да поддържате множество сървъри на база данни, трябва да направите някои корекции в този подход.
Заключение
Започнахте този урок с голяма маса и проблем. Искахте да направите приложението си по-бързо за потребителите си и искахте да направите това, без да им причинявате прекъсвания.
До края на урока успяхте да генерирате и безопасно модифицирате миграция на Django, за да постигнете тази цел. Справихте се с различни проблеми по пътя и успяхте да ги преодолеете с помощта на вградени инструменти, предоставени от рамката за миграции.
В този урок научихте следното:
- Как работят миграциите на Django вътрешно, използвайки състоянието на модела и базата данни и кога се генерират нови миграции
- Как да изпълним персонализиран SQL при миграции с помощта на
RunSQL
действие - Какво представляват обратими миграции и как да се направи
RunSQL
действие обратимо - Какво представляват атомните миграции и как да промените поведението по подразбиране според нуждите си
- Как безопасно да изпълнявате сложни миграции в Django
Разделянето между състоянието на модела и базата данни е важна концепция. След като го разберете и как да го използвате, можете да преодолеете много ограничения на вградените операции за миграция. Някои случаи на употреба, които идват на ум, включват добавяне на индекс, който вече е създаден в базата данни, и предоставяне на специфични за доставчика аргументи към DDL командите.