Как создать миграцию данных 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, которую я использовал в примере, немного наивна, поскольку она может генерировать дубликаты заголовков для большого набора данных. Всегда тестируйте миграцию данных сначала в среде постановки, чтобы избежать поломок в производстве.
Также важно делать это шаг за шагом, чтобы вы могли чувствовать контроль над вносимыми изменениями. Обратите внимание, что здесь я создаю три файла миграции для простого переноса данных.
Как видите, создать такой тип миграции довольно просто. Он также очень гибкий. Например, вы можете загрузить внешний текстовый файл, чтобы вставить данные, например, в новый столбец.
Вернуться на верх