Миграции данных в 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

Миграции с зависимостями

Указывай зависимости между миграциями:

 1class Migration(migrations.Migration):
 2    dependencies = [
 3        ('myapp', '0001_initial'),
 4        ('users', '0002_profile_model'),  # Зависимость от другой миграции
 5        ('contenttypes', '0001_initial'),  # Системная зависимость
 6    ]
 7
 8    operations = [
 9        migrations.RunPython(populate_data),
10    ]

Миграции с транзакциями

Используй транзакции для атомарности:

 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()

Откат миграций

Откатывай миграции пошагово:

 1# Откат к конкретной миграции
 2python manage.py migrate myapp 0003
 3
 4# Откат на одну миграцию назад
 5python manage.py migrate myapp 0002
 6
 7# Откат всех миграций приложения
 8python manage.py migrate myapp zero
 9
10# Просмотр статуса миграций
11python manage.py showmigrations myapp

Тестирование миграций

Создавай тесты для миграций:

 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: Создавай резервные копии, используй транзакции и планируй стратегию отката.