Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Модели: Основы
models_basics

Модели: Основы

Поля, ограничения БД, миграции и жизненный цикл модели

Модели Django: данные, правила и ограничения

Модель описывает данные, которые Django хранит в базе: столбцы, связи, индексы и ограничения. Но модель — не магический валидатор. Важно различать три уровня:

  • поле модели сообщает Django тип данных;
  • форма или serializer проверяет пользовательский ввод;
  • ограничение базы защищает данные даже при конкурентных запросах и обходе формы.

Эти уровни дополняют друг друга.

#Небольшая модель вместо большого листинга

from django.conf import settings from django.db import models from django.db.models import Q from django.utils import timezone class Post(models.Model): class Status(models.TextChoices): DRAFT = "draft", "Черновик" PUBLISHED = "published", "Опубликован" ARCHIVED = "archived", "В архиве" title = models.CharField(max_length=200) slug = models.SlugField(max_length=220, unique=True, allow_unicode=True) content = models.TextField() author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="posts", ) status = models.CharField( max_length=20, choices=Status, default=Status.DRAFT, ) views = models.PositiveBigIntegerField(default=0) published_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["status", "-published_at"]), ] constraints = [ models.CheckConstraint( condition=( Q(status="published", published_at__isnull=False) | ~Q(status="published") ), name="published_post_has_date", ), ] def __str__(self): return self.title def publish(self): self.status = self.Status.PUBLISHED self.published_at = timezone.now() self.save(update_fields=["status", "published_at", "updated_at"])

В примере unique=True уже создаёт ограничение уникальности для slug, поэтому второй UniqueConstraint на то же поле не нужен. Составной индекс соответствует частому запросу: опубликованные записи по дате.

PROTECT у автора выбран сознательно: случайное удаление пользователя не должно каскадно удалить публикации. В другом домене правильным может быть SET_NULL или CASCADE; универсального варианта нет.

#Типы полей выбирают по смыслу

ЗадачаПолеПрактический нюанс
Короткий текстCharFieldУкажите осмысленный max_length
Большой текстTextFieldНе загружайте его в списках без необходимости
Денежная суммаDecimalFieldНе используйте FloatField для денег
СчётчикPositiveBigIntegerFieldПоложительность лучше дополнительно защитить constraint при сложных обновлениях
Дата и времяDateTimeFieldПри USE_TZ=True работайте с aware datetime
Необязательный флагBooleanField(null=True)Убедитесь, что третье состояние действительно имеет смысл
Структурированные данныеJSONFieldНе превращайте JSON в замену нормальной схеме без причины

EmailField, URLField и другие поля имеют validators, но обычный model.save() их не запускает. Проверка происходит в ModelForm, serializer или при явном вызове full_clean().

post = Post(**payload) post.full_clean() post.save()

Даже full_clean() не отменяет database constraint. Между проверкой уникальности и INSERT другой запрос может вставить такое же значение, поэтому приложение должно быть готово обработать IntegrityError.

#null и blank отвечают за разное

null=True разрешает SQL NULL. blank=True разрешает пустое значение на уровне Django validation.

subtitle = models.CharField(max_length=200, blank=True) reviewed_at = models.DateTimeField(null=True, blank=True)

Для строк обычно достаточно пустой строки и blank=True. Одновременные NULL и "" создают два представления «нет значения». Исключение может быть оправдано, если поле уникальное и бизнес-смысл различает эти состояния.

#Значения по умолчанию

Для изменяемого значения передают callable, а не готовый объект:

metadata = models.JSONField(default=dict)

default={} создаст один общий словарь на уровне объявления поля. Аналогично, default=uuid.uuid4 передаёт функцию, а default=uuid.uuid4() вычисляет одно значение при импорте модуля.

#Связи

#ForeignKey

Foreign key хранится на стороне «много»:

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

Удаление поста удалит его комментарии. Это разумно, если комментарий без поста не имеет смысла.

#ManyToManyField

class Tag(models.Model): name = models.CharField(max_length=50, unique=True) class Article(models.Model): tags = models.ManyToManyField(Tag, related_name="articles", blank=True)

Если у связи появляются собственные данные — роль, дата добавления, порядок — нужна явная through-модель с UniqueConstraint.

class Membership(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) team = models.ForeignKey("Team", on_delete=models.CASCADE) role = models.CharField(max_length=20) class Meta: constraints = [ models.UniqueConstraint( fields=["user", "team"], name="unique_team_member", ), ]

#OneToOneField

One-to-one подходит, когда дополнительная запись может существовать не у каждого объекта. Не создавайте отдельный Profile только по привычке: несколько полей часто проще положить в custom user model в начале проекта.

#Slug: удобный URL и источник гонок

Простой slugify(title) не гарантирует уникальность. Два одинаковых заголовка дадут одинаковый slug, а для нелатинского текста поведение зависит от allow_unicode и выбранной транслитерации.

Практические варианты:

  • просить автора задать slug и валидировать его через форму;
  • добавлять стабильный уникальный суффикс;
  • использовать в URL primary key/UUID и оставить slug декоративным;
  • сохранить unique constraint и обработать конфликт при записи.

Не стоит прятать сложный алгоритм разрешения коллизий в save() без тестов: bulk-операции этот метод обходят.

#Атомарные счётчики

Код post.views += 1; post.save() теряет обновления при двух одновременных запросах. Вычисление нужно перенести в БД:

from django.db.models import F Post.objects.filter(pk=post_id).update(views=F("views") + 1)

Экземпляр post, если он уже загружен, после такого update содержит старое значение. При необходимости вызовите refresh_from_db().

#save() и delete() — не глобальные hooks

Переопределение save() влияет на instance.save(), но QuerySet.update(), bulk_create() и часть bulk-сценариев его обходят. То же относится к простому soft delete через Model.delete(): QuerySet.delete() требует отдельной реализации.

Поэтому soft delete — не двухстрочная функция. Нужно заранее решить:

  • должен ли default manager скрывать удалённые строки;
  • как работают reverse relations и admin;
  • что делает cascade;
  • разрешено ли повторное создание объекта с тем же unique-полем;
  • как удалить данные физически по требованиям retention.

Для базового курса безопаснее не показывать неполную реализацию как готовый паттерн.

#Миграции

makemigrations сравнивает модели с состоянием предыдущих миграций и создаёт Python-файл операций. migrate применяет их к базе.

python manage.py makemigrations blog python manage.py sqlmigrate blog 0001 python manage.py migrate

Миграция — часть исходного кода. Её коммитят и проверяют так же, как остальные изменения. На больших таблицах безопасная форма миграции зависит от конкретной СУБД и объёма данных.

#Legacy-пометки

  • Удалено в Django 4.0: NullBooleanField. Замена — BooleanField(null=True).
  • Legacy: Meta.unique_together. Для нового кода используйте UniqueConstraint.
  • Удалено в Django 6.0: аргумент CheckConstraint(check=...). Используйте condition=...; он работает и в Django 5.2.

#Что проверить после главы

  • Вы различаете model validation и database constraint.
  • Для денег выбираете DecimalField, а не float.
  • Понимаете разницу null и blank.
  • Выбираете on_delete по бизнес-смыслу.
  • Не считаете переопределение save() универсальным hook.
  • Обновляете конкурентные счётчики через F().
  • Не обещаете корректный soft delete без QuerySet, relations и unique-policy.

#Документация

  • Models
  • Model fields
  • Model instance validation
  • Constraints

Далее: ORM: Базовые запросы