Как создать миграцию данных Django

Миграция данных - это очень удобный способ изменения данных в базе данных в связи с изменениями в схеме. Они работают как обычная миграция схемы. Django отслеживает зависимости, порядок выполнения и то, применяло ли приложение уже данную миграцию данных или нет.

Частым случаем использования миграции данных является ситуация, когда нам нужно ввести новые поля, которые не являются нулевыми. Или когда мы создаем новое поле для хранения кэшированного подсчета чего-либо, тогда мы можем создать новое поле и добавить начальный подсчет.

В этом посте мы рассмотрим простой пример, который вы можете очень легко расширить и модифицировать для своих нужд.

Миграция данных

Допустим, у нас есть приложение с именем blog, которое установлено в нашем проекте INSTALLED_APPS.

У блога следующее определение модели:

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()

    def __str__(self):
        return self.title

Приложение уже использует эту модель Post; оно уже в производстве, и в базе данных хранится много данных.

id title date content
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […]
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […]
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […]
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […]

Допустим, мы хотим ввести новое поле slug, которое будет использоваться для составления новых URL-адресов блога. Поле slug должно быть уникальным и не нулевым.

Вообще говоря, всегда добавляйте новые поля либо как null=True, либо со значением default. Если мы не можем решить проблему с параметром default, сначала создайте поле как null=True, затем создайте для него миграцию данных. После этого мы можем создать новую миграцию, чтобы установить поле как null=False.

Вот как мы можем это сделать:

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()
    slug = models.SlugField(null=True)

    def __str__(self):
        return self.title

Создайте миграцию:

python manage.py makemigrations blog

Migrations for 'blog':
  blog/migrations/0002_post_slug.py
    - Add field slug to post

Применить его:

python manage.py migrate blog

Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0002_post_slug... OK

На данный момент в базе данных уже есть колонка slug.

id title date content slug
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […] (null)
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […] (null)
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […] (null)
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […] (null)

Создайте пустую миграцию следующей командой:

python manage.py makemigrations blog --empty

Migrations for 'blog':
  blog/migrations/0003_auto_20170926_1105.py

Теперь откройте файл 0003_auto_20170926_1105.py, и он должен иметь следующее содержание:

blog/migrations/0003_auto_20170926_1105.py

# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-26 11:05
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_post_slug'),
    ]

    operations = [
    ]

Затем здесь, в этом файле, мы можем создать функцию, которая может быть выполнена командой RunPython:

blog/migrations/0003_auto_20170926_1105.py

# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-26 11:05
from __future__ import unicode_literals

from django.db import migrations
from django.utils.text import slugify


def slugify_title(apps, schema_editor):
    '''
    We can't import the Post model directly as it may be a newer
    version than this migration expects. We use the historical version.
    '''
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.slug = slugify(post.title)
        post.save()


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_post_slug'),
    ]

    operations = [
        migrations.RunPython(slugify_title),
    ]

В приведенном примере мы используем служебную функцию slugify. Она принимает строку в качестве параметра и преобразует ее в slug. Смотрите ниже несколько примеров:

from django.utils.text import slugify

slugify('Hello, World!')
'hello-world'

slugify('How to Extend the Django User Model')
'how-to-extend-the-django-user-model'

Anyway, функция, используемая методом RunPython для создания миграции данных, ожидает два параметра: apps и schema_editor. RunPython будет передавать эти параметры. Также не забудьте импортировать модели, используя метод apps.get_model('app_name', 'model_name').

Сохраните файл и выполните миграцию, как вы бы сделали это при обычной миграции модели:

python manage.py migrate blog
Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0003_auto_20170926_1105... OK

Теперь если мы проверим базу данных:

id title date content slug
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […] how-to-render-django-form-manually
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […] how-to-use-celery-and-rabbitmq-with-django
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […] how-to-setup-amazon-s3-in-a-django-project
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […] how-to-configure-mailgun-to-send-emails-in-a-django-project

Каждая запись Post имеет значение, поэтому мы можем смело менять переключатель с null=True на null=False. А поскольку все значения уникальны, мы также можем добавить флаг unique=True.

Изменить модель:

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()
    slug = models.SlugField(null=False, unique=True)

    def __str__(self):
        return self.title

Создайте новую миграцию:

python manage.py makemigrations blog

На этот раз вы увидите следующее приглашение:

You are trying to change the nullable field 'slug' on post to non-nullable without a default; we can't do that
(the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL
 operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in models.py
Select an option:

Выберите вариант 2, набрав "2" в терминале.

Migrations for 'blog':
  blog/migrations/0004_auto_20170926_1422.py
    - Alter field slug on post

Теперь мы можем смело применять миграцию:

python manage.py migrate blog
Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0004_auto_20170926_1422... OK

Выводы

Миграция данных иногда бывает непростой задачей. При создании миграции данных для своих проектов всегда сначала изучайте производственные данные. Реализация slugify_title, которую я использовал в примере, немного наивна, поскольку она может генерировать дубликаты заголовков для большого набора данных. Всегда тестируйте миграцию данных сначала в среде постановки, чтобы избежать поломок в производстве.

Также важно делать это шаг за шагом, чтобы вы могли чувствовать контроль над вносимыми изменениями. Обратите внимание, что здесь я создаю три файла миграции для простого переноса данных.

Как видите, создать такой тип миграции довольно просто. Он также очень гибкий. Например, вы можете загрузить внешний текстовый файл, чтобы вставить данные, например, в новый столбец.

Вернуться на верх