Миграция необязательного поля в обязательное

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

В процессе создания миграций вы можете увидеть сообщение ниже:

$ python3 manage.py makemigrations

You are trying to add a non-nullable field `title` to post 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) Quit, and let me add a default in models.py    
Select an option:    

В этой статье мы разберёмся, что это значит и что с этим делать. Рассматривать ситуацию будем на примере сайта-блога. На сайте есть модель Post с единственным полем для текста статьи:

from django.db import models

class Post(models.Model):
    text = models.TextField()

Теперь добавляем поле для заголовка статьи (title):

from django.db import models

class Post(models.Model):
    text = models.TextField()
    title = models.CharField()

После миграций база данных будет выглядеть как в таблице снизу:

При запуске makemigrations возникнет ошибка You are trying to add a non-nullable field without a default — вы пытаетесь добавить обязательное к заполнению поле title, но не указали для него значение по умолчанию.

Для новых статей база данных будет требовать заполнения обоих полей: title и text, но не понятно что делать с теми записями, которые уже есть в базе. Им нужно добавить title, но тогда это поле окажется пустым. База данных такого не разрешит.

Способ первый: Задать значение по умолчанию

Чтобы база данных сама заполнила пустые ячейки можно задать значение по умолчанию. Это можно сделать двумя способами.

Одноразовое значение по умолчанию

Это значение проставится всем незаполненным полям во время миграции. makemigrations предложит вам выбор из двух пунктов, выберите первый. Вам выдадут поле для ввода и там вы можете указать значение, которое проставится во все незаполненные ячейки таблицы. Мы указали строку "default title":

скриншот

Теперь создастся файл миграции. В нём будет зашита эта строка: "default title". После запуска команды migrate база данных будет выглядеть как таблица:

скриншот

Многоразовое значение по умолчанию

Это значение задаёт title не только для старых постов, которые уже лежат в БД, но и для новых с пустым заголовком. Оно задаётся в models.py с помощью аргумента default:

from django.db import models

class Post(models.Model):
    text = models.TextField()
    title = models.CharField(default="default title")

Теперь запуск makemigrations пройдёт успешно и без лишних вопросов. Более того, если при добавлении в базу нового поста вы не укажете title, то он автоматически примет значение по умолчанию:

>>> post = Post.objects.create(text="Текст статьи")
>>> post.text
Текст статьи
>>> post.title
default title

Способ второй: Разрешить полю быть пустым

Поле не заполнено, но, может, и фиг с ним? Некоторым полям и не нужно всегда быть заполненными. В такой ситуации вам поможет null=True и blank=True. Как это работает мы увидим, если добавим ещё одно поле published_at, в котором будет храниться время и дата публикации поста:

from django.db import models

class Post(models.Model):
    text = models.TextField()
    title = models.CharField(blank=True)
    published_at = models.DateTimeField(null=True)

Что такое null=True

Это свойство позволяет базе данных оставлять поле published_at пустым. Это пригодится, если мы добавили пост в базу данных, но ещё не опубликовали. Чтобы опубликовать пост, достаточно добавить ему текущее время в поле published_at:

>>> some_post = Post.objects.create(text="Текст этого поста")
>>> some_post.published_at
None # Сейчас не опубликован, в поле лежит None
>>> some_post.published_at = timezone.now()
>>> some_post.save()  # Теперь опубликован, в поле лежит текущее время

Крайне не рекомендуется применять это свойство к строковым полям (CharField и TextField), так как вместо None в них лучше класть пустую строку. Если указать для строкового поля null=True, то оно может содержать два возможных “пустых” значения: None и пустую строку. Два варианта “пустых” значений создадут путаницу, поэтому в Django разрешают только пустую строку. В этом вам поможет blank=True.

Что такое blank=True

Если null=True управляет поведением базы данных, тоblank=True настраивает поведение админки и Django-форм. Если для поля published_at не указан blank=True, то админка запретит нам сохранить пост, и потребует заполнить поле published_at. Да, админка будет сопротивляться даже если у published_at уже есть null=True.

Способ третий: Если не хочется ставить default или null

Иногда значение по умолчанию или null=True ставить не хочется. Например, в ситуации, когда у вас есть модель поста и модель комментария, и вы решили их связать. Изначально комментария есть только сам его текст:

from django.db import models

class Post(models.Model):
    text = models.TextField()
    title = models.CharField(blank=True)

class Comment(models.Model):
    text = models.TextField()

Теперь вы захотели добавить комментарию ссылку на пост, чтобы знать, к какому посту писался этот комментарий:

class Comment(models.Model):
    text = models.TextField()
    post = models.ForeignKey(Post, on_delete=models.CASCADE)

При запуске makemigrations возникнет ошибка из начала статьи: You are trying to add a non-nullable field without a default. Проблему можно обойти через добавление атрибутов null=True или default, но это неправильно, ведь на самом деле у комментария поле post не должно пустовать ни при каких условиях.

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

В такой ситуации есть 2 пути: простой, но варварский и хороший, но сложный.

Простой, но варварский способ решения проблемы

Если вы только проектируете БД и в ней нет никаких важных данных, то самое простое — это удалить все миграции и саму БД. После этого смело создавайте миграции и мигрируйтесь. Получится так, будто вы с самого начала создали модель с ForeignKey, а значит и пустым это поле никогда не было.

Если вы работаете вместе с другими программистами, никогда так не делайте, потому что это вызовет у них большие проблемы: им тоже придётся удалять свои базы данных. Но если вы в проекте один, а в базе данных нет ничего важного — это хороший способ быстро всё исправить.

Сложный, но правильный способ решения проблемы

Если вы работаете с другими программистами и/или в БД уже есть важные данные, то удалять базу — не вариант. Тогда делается следующее:

1. Создайте новую модель PostComment рядом с существующей. В новой модели укажите связь — ForeignKey:

class Comment(models.Model):
    text = models.TextField()

class PostComment(models.Model):
    text = models.TextField()
    post = models.ForeignKey(Post, on_delete=models.CASCADE)

2. Сделайте и примените миграцию с помощью makemigrations и migrate.

3. Скопируйте данные из Comment в PostComment с помощью дата-миграции. Это специальная миграция, которая просто запустит тот код Python, который вы сами напишете. Вам придётся придумать способ как перенести старые данные на новую модель.

4. Запустите дата-миграцию, чтобы скопировать данные из Comment в PostComment.

5. Теперь, когда все важные данные спасены, можно удалить модель Comment. После миграции с удалением старой модели можно переименовать PostComment к старому названию Comment и провести ещё одну миграцию.


Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.