Безопасные миграции в Django
Миграции в production требуют особой осторожности, чтобы не потерять данные пользователей. Неправильно выполненная миграция может привести к простою сервиса и потере критически важной информации.
Основные принципы безопасности
- Всегда делай backup перед миграцией - это твоя страховка
- Тестируй миграции на копии production базы - никогда не тестируй на живых данных
- Используй поэтапные миграции для крупных изменений - разбивай сложные изменения на простые шаги
- Планируй время выполнения - большие миграции могут занять часы
- Имей план отката - знай как вернуться к предыдущему состоянию
Пошаговый процесс безопасной миграции
Шаг 1: Подготовка и планирование
1# 1. Создай backup базы данных
2pg_dump -h localhost -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
3
4# 2. Проверь текущее состояние миграций
5python manage.py showmigrations
6
7# 3. Создай копию production базы для тестирования
8createdb test_database_name
9psql -d test_database_name < backup_file.sql
10
11# 4. Обнови settings.py для тестовой базы
12DATABASES = {
13 'default': {
14 'ENGINE': 'django.db.backends.postgresql',
15 'NAME': 'test_database_name',
16 'USER': 'username',
17 'PASSWORD': 'password',
18 'HOST': 'localhost',
19 'PORT': '5432',
20 }
21}
Шаг 2: Создание и тестирование миграции
1# 1. Создай миграцию
2python manage.py makemigrations
3
4# 2. Проверь созданную миграцию
5python manage.py sqlmigrate app_name 0002
6
7# 3. Протестируй миграцию на копии
8python manage.py migrate
9
10# 4. Проверь целостность данных
11python manage.py check
12python manage.py shell
13
14# В shell проверь данные
15from myapp.models import MyModel
16print(MyModel.objects.count())
17print(MyModel.objects.first())
Практические примеры безопасных миграций
Пример 1: Добавление нового поля с default значением
1# models.py - добавляем новое поле
2class User(models.Model):
3 username = models.CharField(max_length=100)
4 email = models.EmailField()
5 # Новое поле
6 is_verified = models.BooleanField(default=False, help_text='Подтвержден ли email')
7 created_at = models.DateTimeField(auto_now_add=True)
8
9# Создаем миграцию
10python manage.py makemigrations
11
12# Получаем что-то вроде:
13# migrations/0002_user_is_verified.py
14from django.db import migrations, models
15
16class Migration(migrations.Migration):
17 dependencies = [
18 ('users', '0001_initial'),
19 ]
20
21 operations = [
22 migrations.AddField(
23 model_name='user',
24 name='is_verified',
25 field=models.BooleanField(default=False, help_text='Подтвержден ли email'),
26 ),
27 ]
Пример 2: Изменение типа поля с сохранением данных
1# Исходная модель
2class Product(models.Model):
3 name = models.CharField(max_length=200)
4 price = models.DecimalField(max_digits=10, decimal_places=2) # Старый тип
5
6# Новая модель - хотим изменить на IntegerField (копейки)
7class Product(models.Model):
8 name = models.CharField(max_length=200)
9 price = models.IntegerField(help_text='Цена в копейках') # Новый тип
10
11# Создаем поэтапную миграцию
12# Шаг 1: Добавляем новое поле
13class Migration(migrations.Migration):
14 dependencies = [
15 ('products', '0001_initial'),
16 ]
17
18 operations = [
19 migrations.AddField(
20 model_name='product',
21 name='price_cents',
22 field=models.IntegerField(default=0, help_text='Цена в копейках'),
23 ),
24 ]
25
26# Шаг 2: Переносим данные
27def convert_price_to_cents(apps, schema_editor):
28 Product = apps.get_model('products', 'Product')
29 for product in Product.objects.all():
30 # Конвертируем Decimal в копейки
31 product.price_cents = int(product.price * 100)
32 product.save()
33
34def reverse_convert_price_to_cents(apps, schema_editor):
35 Product = apps.get_model('products', 'Product')
36 for product in Product.objects.all():
37 # Конвертируем обратно в Decimal
38 product.price = product.price_cents / 100
39 product.save()
40
41class Migration(migrations.Migration):
42 dependencies = [
43 ('products', '0002_product_price_cents'),
44 ]
45
46 operations = [
47 migrations.RunPython(convert_price_to_cents, reverse_convert_price_to_cents),
48 ]
49
50# Шаг 3: Удаляем старое поле
51class Migration(migrations.Migration):
52 dependencies = [
53 ('products', '0003_convert_price_to_cents'),
54 ]
55
56 operations = [
57 migrations.RemoveField(
58 model_name='product',
59 name='price',
60 ),
61 migrations.RenameField(
62 model_name='product',
63 old_name='price_cents',
64 new_name='price',
65 ),
66 ]
Пример 3: Безопасное переименование поля
1# Исходная модель
2class Article(models.Model):
3 title = models.CharField(max_length=200)
4 content = models.TextField()
5 pub_date = models.DateTimeField() # Старое название
6
7# Новая модель
8class Article(models.Model):
9 title = models.CharField(max_length=200)
10 content = models.TextField()
11 published_at = models.DateTimeField() # Новое название
12
13# Создаем миграцию с переименованием
14class Migration(migrations.Migration):
15 dependencies = [
16 ('blog', '0001_initial'),
17 ]
18
19 operations = [
20 migrations.RenameField(
21 model_name='article',
22 old_name='pub_date',
23 new_name='published_at',
24 ),
25 ]
Пример 4: Добавление NOT NULL поля к существующей таблице
1# Исходная модель
2class Order(models.Model):
3 customer_name = models.CharField(max_length=100)
4 total_amount = models.DecimalField(max_digits=10, decimal_places=2)
5
6# Новая модель - добавляем обязательное поле
7class Order(models.Model):
8 customer_name = models.CharField(max_length=100)
9 total_amount = models.DecimalField(max_digits=10, decimal_places=2)
10 status = models.CharField(max_length=20, choices=[
11 ('pending', 'В ожидании'),
12 ('confirmed', 'Подтвержден'),
13 ('shipped', 'Отправлен'),
14 ('delivered', 'Доставлен'),
15 ])
16
17# Поэтапная миграция
18# Шаг 1: Добавляем поле с default значением
19class Migration(migrations.Migration):
20 dependencies = [
21 ('orders', '0001_initial'),
22 ]
23
24 operations = [
25 migrations.AddField(
26 model_name='order',
27 name='status',
28 field=models.CharField(
29 max_length=20,
30 choices=[
31 ('pending', 'В ожидании'),
32 ('confirmed', 'Подтвержден'),
33 ('shipped', 'Отправлен'),
34 ('delivered', 'Доставлен'),
35 ],
36 default='pending', # Временное default значение
37 ),
38 ),
39 ]
40
41# Шаг 2: Заполняем поле осмысленными данными
42def set_order_status(apps, schema_editor):
43 Order = apps.get_model('orders', 'Order')
44 for order in Order.objects.all():
45 # Логика определения статуса на основе существующих данных
46 if order.total_amount > 0:
47 order.status = 'confirmed'
48 else:
49 order.status = 'pending'
50 order.save()
51
52class Migration(migrations.Migration):
53 dependencies = [
54 ('orders', '0002_order_status'),
55 ]
56
57 operations = [
58 migrations.RunPython(set_order_status),
59 ]
60
61# Шаг 3: Убираем default и делаем поле обязательным
62class Migration(migrations.Migration):
63 dependencies = [
64 ('orders', '0003_set_order_status'),
65 ]
66
67 operations = [
68 migrations.AlterField(
69 model_name='order',
70 name='status',
71 field=models.CharField(
72 max_length=20,
73 choices=[
74 ('pending', 'В ожидании'),
75 ('confirmed', 'Подтвержден'),
76 ('shipped', 'Отправлен'),
77 ('delivered', 'Доставлен'),
78 ],
79 # Убираем default
80 ),
81 ),
82 ]
Проверка и валидация миграций
Команды для проверки
1# Проверка состояния миграций
2python manage.py showmigrations
3
4# Проверка SQL без выполнения
5python manage.py sqlmigrate app_name 0002
6
7# Проверка целостности проекта
8python manage.py check
9
10# Проверка миграций на конфликты
11python manage.py makemigrations --dry-run
12
13# Проверка зависимостей миграций
14python manage.py migrate --plan
Тестирование миграций
1# Создаем тестовую базу
2python manage.py test --keepdb
3
4# Или создаем отдельную тестовую базу
5DATABASES = {
6 'default': {
7 'ENGINE': 'django.db.backends.postgresql',
8 'NAME': 'test_db',
9 },
10 'test': {
11 'ENGINE': 'django.db.backends.postgresql',
12 'NAME': 'test_db_test',
13 }
14}
15
16# Запускаем тесты с новой базой
17python manage.py test --database=test
План отката (Rollback)
Откат к предыдущей миграции
1# Откатываемся к конкретной миграции
2python manage.py migrate app_name 0001
3
4# Откатываемся на одну миграцию назад
5python manage.py migrate app_name 0002
6
7# Проверяем текущее состояние
8python manage.py showmigrations app_name
9
10# Восстанавливаем базу из backup если нужно
11psql -d database_name < backup_file.sql
Создание миграции для отката
Миграции в production
Подготовка к production миграции
1# 1. Выбери время минимальной нагрузки
2# 2. Уведоми команду о планируемом простое
3# 3. Подготовь rollback план
4# 4. Создай backup
5
6# Команда для создания backup
7pg_dump -h production_host -U username -d production_db > production_backup_$(date +%Y%m%d_%H%M%S).sql
8
9# Проверь размер backup
10ls -lh production_backup_*.sql
11
12# Проверь время выполнения на тестовой базе
13time python manage.py migrate
Выполнение production миграции
1# 1. Останови приложение (если нужно)
2sudo systemctl stop gunicorn
3sudo systemctl stop celery
4
5# 2. Выполни миграцию
6python manage.py migrate
7
8# 3. Проверь результат
9python manage.py check
10python manage.py showmigrations
11
12# 4. Запусти приложение
13sudo systemctl start gunicorn
14sudo systemctl start celery
15
16# 5. Проверь работоспособность
17curl -I https://yoursite.com/health/
Частые ошибки и их решения
Ошибка: "django.db.utils.IntegrityError: column cannot contain NULL values"
1# Проблема: добавляешь NOT NULL поле к таблице с данными
2# Решение: используй поэтапную миграцию
3
4# Шаг 1: Добавь поле с default значением
5migrations.AddField(
6 model_name='mymodel',
7 name='new_field',
8 field=models.CharField(max_length=100, default='default_value'),
9)
10
11# Шаг 2: Заполни поле данными
12def populate_field(apps, schema_editor):
13 MyModel = apps.get_model('myapp', 'MyModel')
14 for obj in MyModel.objects.all():
15 obj.new_field = 'calculated_value'
16 obj.save()
17
18migrations.RunPython(populate_field)
19
20# Шаг 3: Сделай поле обязательным
21migrations.AlterField(
22 model_name='mymodel',
23 name='new_field',
24 field=models.CharField(max_length=100), # Убираем default
25)
Ошибка: "django.db.utils.OperationalError: cannot ALTER TABLE"
1# Проблема: PostgreSQL не может изменить таблицу
2# Решение: используй RunSQL для сложных изменений
3
4class Migration(migrations.Migration):
5 dependencies = [
6 ('myapp', '0001_initial'),
7 ]
8
9 operations = [
10 migrations.RunSQL(
11 # SQL для изменения таблицы
12 "ALTER TABLE myapp_mymodel ADD COLUMN new_field VARCHAR(100) DEFAULT 'default';",
13 # SQL для отката
14 "ALTER TABLE myapp_mymodel DROP COLUMN new_field;",
15 ),
16 ]
Лучшие практики
- Тестируй на копии production данных - никогда не тестируй миграции на живых данных
- Используй поэтапные миграции - разбивай сложные изменения на простые шаги
- Создавай осмысленные названия миграций - это поможет в отладке
- Документируй сложные миграции - добавляй комментарии в код миграции
- Планируй время выполнения - большие миграции могут занять часы
- Имей план отката - знай как вернуться к предыдущему состоянию
- Мониторь производительность - следи за временем выполнения миграций
FAQ
Q: Что делать если миграция упала?
A: Проверь состояние через showmigrations, откатись к предыдущей версии, исправь ошибку и создай новую миграцию.
Q: Как откатить миграцию в production?
A: Используй python manage.py migrate app_name previous_migration_number или восстанови базу из backup.
Q: Можно ли изменять уже примененную миграцию?
A: Нет, никогда не изменяй уже примененные миграции. Создай новую миграцию для исправления.
Q: Как безопасно изменить тип поля?
A: Используй поэтапную миграцию: добавь новое поле, перенеси данные, удали старое поле.
Q: Что делать если миграция зависла?
A: Проверь логи, возможно есть блокировка таблицы. В крайнем случае перезапусти PostgreSQL.
Q: Как мигрировать большие таблицы без простоя?
A: Используй поэтапные миграции, выполняй в нерабочее время, используй инструменты типа pt-online-schema-change.