Миграция необязательного поля в обязательное
В этой статье будем разбирать сложные случаи при миграциях. Если вы не знаете, что это, вот статья о миграциях.
В процессе создания миграций вы можете увидеть сообщение ниже:
$ 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
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.