Миграции данных в Django
Миграции данных позволяют изменять содержимое базы данных вместе со схемой. Они незаменимы для обновления существующих данных, заполнения новых полей или исправления данных в продакшене.
Создание миграции данных
Создай миграцию с помощью команды:
1python manage.py makemigrations --empty myapp --name populate_books
Затем добавь логику в созданный файл:
1from django.db import migrations
2
3def populate_books(apps, schema_editor):
4 Book = apps.get_model('myapp', 'Book')
5 Author = apps.get_model('myapp', 'Author')
6
7 # Получаем или создаем автора по умолчанию
8 default_author, created = Author.objects.get_or_create(
9 name='Default Author',
10 defaults={'email': 'default@example.com'}
11 )
12
13 # Создаем книги
14 for i in range(100):
15 Book.objects.create(
16 title=f'Book {i}',
17 author=default_author,
18 price=10.99 + i,
19 is_available=True
20 )
21
22def reverse_populate_books(apps, schema_editor):
23 Book = apps.get_model('myapp', 'Book')
24 Book.objects.filter(author__name='Default Author').delete()
25
26class Migration(migrations.Migration):
27 dependencies = [
28 ('myapp', '0001_initial'),
29 ]
30
31 operations = [
32 migrations.RunPython(populate_books, reverse_populate_books),
33 ]
Типы миграций данных
RunPython - выполнение Python кода:
1def update_user_status(apps, schema_editor):
2 User = apps.get_model('auth', 'User')
3
4 # Обновляем статус пользователей
5 User.objects.filter(is_active=True).update(
6 date_joined=timezone.now()
7 )
8
9class Migration(migrations.Migration):
10 operations = [
11 migrations.RunPython(update_user_status),
12 ]
RunSQL - выполнение SQL запросов:
1class Migration(migrations.Migration):
2 operations = [
3 migrations.RunSQL(
4 # SQL для применения
5 sql="""
6 UPDATE myapp_book
7 SET price = price * 1.1
8 WHERE category = 'fiction'
9 """,
10 # SQL для отката
11 reverse_sql="""
12 UPDATE myapp_book
13 SET price = price / 1.1
14 WHERE category = 'fiction'
15 """
16 ),
17 ]
Работа с моделями в миграциях
Важно использовать apps.get_model() вместо прямого импорта:
1def migrate_user_data(apps, schema_editor):
2 # Правильно - получаем модель через apps
3 User = apps.get_model('auth', 'User')
4 Profile = apps.get_model('users', 'Profile')
5
6 # Неправильно - прямой импорт
7 # from users.models import Profile
8
9 for user in User.objects.all():
10 Profile.objects.get_or_create(
11 user=user,
12 defaults={
13 'bio': f'Profile for {user.username}',
14 'avatar': 'default.jpg'
15 }
16 )
Обработка ошибок в миграциях
Добавляй проверки и обработку ошибок:
1def safe_update_books(apps, schema_editor):
2 Book = apps.get_model('myapp', 'Book')
3
4 try:
5 # Проверяем, что поле существует
6 if hasattr(Book, 'new_field'):
7 for book in Book.objects.all():
8 try:
9 book.new_field = 'default_value'
10 book.save()
11 except Exception as e:
12 print(f"Ошибка обновления книги {book.id}: {e}")
13
14 except Exception as e:
15 print(f"Критическая ошибка в миграции: {e}")
16 # Можно вызвать исключение для остановки миграции
17 # raise e
Миграции с зависимостями
Указывай зависимости между миграциями:
Миграции с транзакциями
Используй транзакции для атомарности:
1from django.db import migrations, transaction
2
3def update_prices_in_transaction(apps, schema_editor):
4 Book = apps.get_model('myapp', 'Book')
5
6 with transaction.atomic():
7 # Все операции выполняются в одной транзакции
8 for book in Book.objects.all():
9 book.price = book.price * 1.15
10 book.save()
11
12 # Если что-то пойдет не так, все изменения откатятся
Миграции с большими объемами данных
Для больших данных используй batch processing:
1def update_large_dataset(apps, schema_editor):
2 Book = apps.get_model('myapp', 'Book')
3 batch_size = 1000
4
5 # Обрабатываем данные батчами
6 for offset in range(0, Book.objects.count(), batch_size):
7 batch = Book.objects.all()[offset:offset + batch_size]
8
9 for book in batch:
10 book.status = 'active'
11 book.save()
12
13 print(f"Обработано {offset + len(batch)} книг")
Миграции с внешними данными
Импорт данных из внешних источников:
1import csv
2from django.core.files.base import ContentFile
3
4def import_books_from_csv(apps, schema_editor):
5 Book = apps.get_model('myapp', 'Book')
6 Author = apps.get_model('myapp', 'Author')
7
8 csv_file = ContentFile(b'''
9 title,author,price
10 "Book 1","Author 1",19.99
11 "Book 2","Author 2",24.99
12 ''')
13
14 reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines())
15
16 for row in reader:
17 author, created = Author.objects.get_or_create(
18 name=row['author']
19 )
20
21 Book.objects.create(
22 title=row['title'],
23 author=author,
24 price=float(row['price'])
25 )
Миграции с валидацией данных
Проверяй данные перед сохранением:
1def validate_and_update_books(apps, schema_editor):
2 Book = apps.get_model('myapp', 'Book')
3
4 for book in Book.objects.all():
5 # Валидация данных
6 if not book.title or len(book.title.strip()) == 0:
7 book.title = f"Untitled Book {book.id}"
8
9 if book.price < 0:
10 book.price = 0
11
12 if book.price > 1000:
13 book.price = 1000
14
15 book.save()
Откат миграций
Откатывай миграции пошагово:
Тестирование миграций
Создавай тесты для миграций:
1from django.test import TestCase
2from django.db.migrations.executor import MigrationExecutor
3from django.db import connection
4
5class MigrationTestCase(TestCase):
6 def test_migration_forward(self):
7 # Применяем миграцию
8 executor = MigrationExecutor(connection)
9 executor.migrate([('myapp', '0002_populate_data')])
10
11 # Проверяем результат
12 Book = apps.get_model('myapp', 'Book')
13 self.assertEqual(Book.objects.count(), 100)
14
15 # Откатываем миграцию
16 executor.migrate([('myapp', '0001_initial')])
Миграции в продакшене
Безопасное применение миграций:
1def safe_production_migration(apps, schema_editor):
2 Book = apps.get_model('myapp', 'Book')
3
4 # Создаем резервную копию
5 backup_data = []
6 for book in Book.objects.all():
7 backup_data.append({
8 'id': book.id,
9 'title': book.title,
10 'price': book.price
11 })
12
13 # Применяем изменения
14 try:
15 for book in Book.objects.all():
16 book.new_field = 'value'
17 book.save()
18 except Exception as e:
19 # Восстанавливаем данные
20 for backup in backup_data:
21 Book.objects.filter(id=backup['id']).update(
22 title=backup['title'],
23 price=backup['price']
24 )
25 raise e
Лучшие практики
- Всегда создавай функции отката для RunPython
- Используй apps.get_model() вместо прямого импорта
- Добавляй обработку ошибок и логирование
- Тестируй миграции на тестовых данных
- Используй транзакции для атомарности
- Обрабатывай большие данные батчами
- Создавай резервные копии перед критическими изменениями
- Документируй сложную логику миграций
FAQ
Q: Как откатить миграцию данных?
A: Используй python manage.py migrate myapp 0001 для отката к предыдущей миграции.
Q: Можно ли изменять модели в миграциях данных?
A: Нет, миграции данных предназначены только для изменения содержимого, не структуры.
Q: Как обработать ошибки в миграциях?
A: Используй try-except блоки и логирование для безопасной обработки ошибок.
Q: Можно ли использовать внешние библиотеки в миграциях?
A: Да, но убедись, что они доступны в окружении, где выполняются миграции.
Q: Как тестировать миграции?
A: Создавай тесты, которые применяют и откатывают миграции, проверяя результат.
Q: Что делать, если миграция упала в продакшене?
A: Создавай резервные копии, используй транзакции и планируй стратегию отката.