Django-Admin.ru

Модели в Django: поля, Meta, __str__ и choices

Как описывать модели Django: типы полей и их параметры, __str__, класс Meta, choices через TextChoices и IntegerChoices, переопределение save() и миграции.

Редакция Django-Admin.ru
· обновлено · 12 мин чтения

Содержание · 13 разделов
  1. Первая модель
  2. Типы полей Django
  3. IntegerField и его варианты
  4. Параметры полей
  5. Метод __str__
  6. Класс Meta
  7. Choices: TextChoices и IntegerChoices
  8. Метод save() и его переопределение
  9. Связи между моделями
  10. values(): данные модели без объектов
  11. Миграции
  12. Частые ошибки
  13. TypeError: missing 1 required positional argument: ‘on_delete’
  14. Объекты отображаются как «Product object (1)»
  15. Изменил модель, а в базе ничего не поменялось
  16. null=True на ManyToManyField
  17. Изменяемый объект в default
  18. Два товара с одинаковым названием ломают уникальный slug
  19. Частые вопросы
  20. Чем абстрактная модель отличается от proxy-модели?
  21. Как добавить вычисляемое поле?
  22. Как сделать свой менеджер, например только опубликованные товары?
  23. Можно ли сделать первичным ключом UUID?
  24. Как разбить models.py на несколько файлов?
  25. Итог

Модель Django (models.Model) — это Python-класс, который описывает таблицу базы данных: каждый атрибут-поле становится колонкой, а экземпляр класса — строкой. По моделям Django создаёт миграции, строит запросы ORM, формы и админку.

Короткий ответ: унаследуйтесь от models.Model, объявите поля (CharField, IntegerField, ForeignKey и т. д.), добавьте __str__ и класс Meta, затем выполните makemigrations и migrate. Ниже — типы полей, их параметры, Meta, choices, переопределение save() и частые ошибки. Примеры проверены на Django 5.2 и 6.1.

Первая модель

Модели живут в файле models.py приложения (как создать приложение — в статье про startapp):

# shop/models.py
from django.db import models


class Category(models.Model):
    name = models.CharField("Название", max_length=100, unique=True)

    class Meta:
        verbose_name = "категория"
        verbose_name_plural = "категории"
        ordering = ["name"]

    def __str__(self):
        return self.name

Поле id создаётся автоматически. Его тип задаёт настройка DEFAULT_AUTO_FIELD. В Django 5.2 её значение по умолчанию — AutoField, поэтому шаблон startproject явно прописывает в settings.py BigAutoField. С Django 6.0 BigAutoField стал значением по умолчанию, и эту строку из шаблона убрали.

Дальше в статье — модель товара, на которой разберём все возможности:

import uuid

from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
from django.utils.text import slugify


class Product(models.Model):
    class Status(models.TextChoices):
        DRAFT = "draft", "Черновик"
        PUBLISHED = "published", "Опубликован"
        ARCHIVED = "archived", "В архиве"

    class Priority(models.IntegerChoices):
        LOW = 1, "Низкий"
        NORMAL = 2, "Обычный"
        HIGH = 3, "Высокий"

    title = models.CharField("Название", max_length=200)
    slug = models.SlugField(max_length=200, unique=True, blank=True)
    description = models.TextField("Описание", blank=True)
    price = models.DecimalField("Цена", max_digits=10, decimal_places=2)
    discount = models.PositiveSmallIntegerField(
        "Скидка, %", default=0, validators=[MaxValueValidator(90)]
    )
    stock = models.PositiveIntegerField("Остаток", default=0)
    rating = models.IntegerField(
        "Рейтинг", null=True, blank=True,
        validators=[MinValueValidator(1), MaxValueValidator(5)],
    )
    status = models.CharField(
        "Статус", max_length=10, choices=Status, default=Status.DRAFT
    )
    priority = models.IntegerField(choices=Priority, default=Priority.NORMAL)
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT, related_name="products", verbose_name="Категория"
    )
    external_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    attributes = models.JSONField(default=dict, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "товар"
        verbose_name_plural = "товары"
        ordering = ["-created_at"]
        indexes = [models.Index(fields=["status", "-created_at"])]
        constraints = [
            models.CheckConstraint(condition=models.Q(price__gte=0), name="product_price_gte_0"),
            models.UniqueConstraint(fields=["category", "title"], name="unique_title_in_category"),
        ]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("product-detail", kwargs={"slug": self.slug})

Типы полей Django

ПолеЧто хранитОбязательные параметры
CharFieldкороткая строкаmax_length
TextFieldдлинный текст—
IntegerFieldцелое от −2147483648 до 2147483647—
SmallIntegerField, BigIntegerFieldменьший / 64-битный диапазон—
PositiveIntegerField, PositiveSmallIntegerFieldцелое ≥ 0—
DecimalFieldточное десятичное (деньги)max_digits, decimal_places
FloatFieldчисло с плавающей точкой—
BooleanFieldTrue/False—
DateField, DateTimeField, TimeFieldдата, дата и время, время—
EmailField, URLField, SlugFieldстроки с валидацией формата—
UUIDFieldUUID—
JSONFieldJSON (словари, списки)—
FileField, ImageFieldпуть к файлу в хранилищеImageField требует Pillow
ForeignKey, OneToOneFieldсвязь с другой модельюмодель и on_delete
ManyToManyFieldсвязь «многие ко многим»модель

IntegerField и его варианты

Целочисленных полей несколько, и выбирать стоит по диапазону значений:

  • IntegerField — от −2147483648 до 2147483647, подходит для большинства счётчиков;
  • SmallIntegerField — от −32768 до 32767, PositiveSmallIntegerField — от 0 до 32767;
  • PositiveIntegerField — от 0 до 2147483647; в PostgreSQL и SQLite Django добавляет в таблицу ограничение CHECK (... >= 0), в MySQL создаёт беззнаковую колонку;
  • BigIntegerField — 64-битное целое для больших значений.

Ограничить диапазон внутри типа можно валидаторами MinValueValidator и MaxValueValidator, как у полей rating и discount выше. Для денег используйте DecimalField, а не FloatField: у float бывают ошибки округления.

Параметры полей

ПараметрЧто делает
null=Trueразрешает NULL в базе
blank=Trueразрешает пустое значение в формах и full_clean()
defaultзначение по умолчанию (значение или вызываемый объект, например dict)
db_defaultзначение по умолчанию на стороне базы (Django 5.0+)
unique=Trueуникальность на уровне базы
db_index=Trueиндекс по полю
choicesограниченный набор значений
verbose_name, help_textподпись и подсказка в формах и админке
editable=Falseполе не показывается в формах и админке
validatorsсписок функций-валидаторов

null относится к базе данных, blank — к валидации. Для строковых полей Django рекомендует не использовать null=True: пустое значение хранится как "", иначе у поля будет два «пустых» состояния. Для дат, чисел и внешних ключей необязательное поле обычно требует обоих параметров: null=True, blank=True.

Метод __str__

__str__ определяет, как объект выглядит в админке, выпадающих списках, шаблонах и shell:

>>> p = Product.objects.create(title="Война и мир", price="990.00", category=books)
>>> print(p)
Война и мир
>>> p
<Product: Война и мир>

Без __str__ вы увидите Product object (1). Возвращайте строку и не обращайтесь в __str__ к связанным моделям: в списках админки это вызовет лишний запрос на каждую строку (подробнее — в статье про N+1 в Django ORM).

Класс Meta

Вложенный класс Meta задаёт поведение модели в целом, а не отдельного поля:

ОпцияНазначение
orderingсортировка по умолчанию, "-поле" — по убыванию
verbose_name, verbose_name_pluralназвание модели в единственном и множественном числе (для админки)
db_tableимя таблицы вместо <приложение>_<модель>
indexesсоставные и частичные индексы
constraintsограничения базы: UniqueConstraint, CheckConstraint
abstract = Trueабстрактная модель для наследования, без своей таблицы
permissionsдополнительные права доступа
>>> Product._meta.verbose_name_plural, Product._meta.ordering, Product._meta.db_table
('товары', ['-created_at'], 'shop_product')

Ограничения проверяет база данных — нарушение даст IntegrityError:

>>> Product.objects.create(title="Y", price=-1, category=books)
django.db.utils.IntegrityError: CHECK constraint failed: product_price_gte_0

У CheckConstraint параметр условия называется condition (Django 5.1+). До 5.1 он назывался check; в Django 6.0 старое имя удалили. Вместо устаревшего unique_together используйте UniqueConstraint, а вместо index_together (удалён в Django 5.1) — indexes.

Важно: ordering в Meta добавляет ORDER BY к каждому запросу модели. На больших таблицах сортировка по полю без индекса замедляет списки — добавьте индекс, как для status, -created_at выше.

Choices: TextChoices и IntegerChoices

Перечисления TextChoices и IntegerChoices — рекомендуемый способ задать набор значений:

>>> Product.Status.choices
[('draft', 'Черновик'), ('published', 'Опубликован'), ('archived', 'В архиве')]
>>> Product.Status.PUBLISHED.label
'Опубликован'
>>> Product.Status.values
['draft', 'published', 'archived']
>>> Product.Status.PUBLISHED == "published"
True

В базе хранится значение ("draft", 2), а подпись выводит автоматически созданный метод get_<поле>_display():

>>> p.status, p.get_status_display(), p.get_priority_display()
('draft', 'Черновик', 'Обычный')
>>> Product.objects.filter(status=Product.Status.PUBLISHED)

В шаблоне — {{ product.get_status_display }}. С Django 5.0 в choices можно передать сам класс (choices=Status); в 4.2 пишите choices=Status.choices. Работает и обычный список кортежей [("draft", "Черновик"), ...].

Учтите: choices проверяется формами и full_clean(), но не базой. Product.objects.create(status="unknown") сохранит недопустимое значение. Если нужна гарантия на уровне базы, добавьте CheckConstraint с condition=models.Q(status__in=Product.Status.values).

Метод save() и его переопределение

save() выполняет INSERT для нового объекта (у которого ещё нет pk) и UPDATE для существующего:

>>> p = Product(title="Без slug", price=1, category=books)
>>> p.pk is None
True
>>> p.save()
>>> p.pk
3
>>> p.price = 2
>>> p.save(update_fields=["price"])   # UPDATE только одной колонки

Метод переопределяют, чтобы заполнить вычисляемые поля перед сохранением. Обязательно вызывайте super().save() и передавайте аргументы дальше:

class Product(models.Model):
    # ...

    def save(self, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title, allow_unicode=True)
        update_fields = kwargs.get("update_fields")
        if update_fields is not None and "title" in update_fields:
            kwargs["update_fields"] = {"slug"}.union(update_fields)
        super().save(**kwargs)
  • Если вызывающий код передал update_fields, изменённый в save() slug не сохранится, пока вы не добавите его в этот список — для этого нужен блок с update_fields.
  • slugify("Война и мир") возвращает пустую строку: кириллица отбрасывается. С allow_unicode=True получится война-и-мир.
  • Позиционные аргументы save() объявлены устаревшими в Django 5.1 и удалены в 6.0: метод принимает только именованные. Сигнатура save(self, **kwargs) подходит для всех актуальных версий.

Важно: QuerySet.update(), bulk_create() и bulk_update() не вызывают save() модели. Логика из переопределённого save() при них не сработает — переносите её в отдельную функцию или вызывайте явно.

Также save() не запускает валидацию полей. Валидаторы, choices и blank проверяет full_clean() (его автоматически вызывают формы Django и админка):

>>> Product(title="X", price=-1, category=books, discount=95, rating=7, status="unknown").full_clean()
ValidationError: {'discount': ['Ensure this value is less than or equal to 90.'],
 'rating': ['Ensure this value is less than or equal to 5.'],
 'status': ["Value 'unknown' is not a valid choice."],
 '__all__': ['Constraint “product_price_gte_0” is violated.']}

Как это использовать в формах — в статье про формы Django.

Связи между моделями

  • ForeignKey — «многие к одному»: у товара одна категория, у категории много товаров. Параметр on_delete обязателен: CASCADE удаляет зависимые записи, PROTECT запрещает удаление (ProtectedError), SET_NULL обнуляет ссылку (нужен null=True).
  • OneToOneField — «один к одному», например профиль пользователя.
  • ManyToManyField — «многие ко многим»: товары и теги. Django создаёт промежуточную таблицу; свою задают через through.

related_name задаёт имя обратной связи: category.products.all(). Ссылаться на пользователя нужно через settings.AUTH_USER_MODEL, см. аутентификацию в Django. Запросы через связи (filter(category__name="Книги")), select_related и prefetch_related разобраны в шпаргалке по Django ORM.

values(): данные модели без объектов

Когда нужны только данные, values() и values_list() возвращают словари и кортежи вместо экземпляров модели:

>>> Product.objects.values("title", "price", "category__name")
<QuerySet [{'title': 'Война и мир', 'price': Decimal('990.00'), 'category__name': 'Книги'}]>
>>> Product.objects.values_list("title", flat=True)
<QuerySet ['Война и мир']>

__str__, свойства и методы модели при этом недоступны. Другие способы выборки — в статье о фильтрации в Django ORM.

Миграции

После любого изменения моделей создайте и примените миграцию:

python manage.py makemigrations shop   # создать файл миграции по изменениям моделей
python manage.py migrate               # применить миграции к базе
python manage.py sqlmigrate shop 0001  # посмотреть SQL миграции
python manage.py showmigrations shop   # какие миграции применены

Файлы миграций храните в git вместе с кодом. Подробно о конфликтах, откате и миграциях данных — в статье про миграции Django.

Частые ошибки

TypeError: missing 1 required positional argument: ‘on_delete’

У ForeignKey и OneToOneField нет значения on_delete по умолчанию — укажите его явно.

Объекты отображаются как «Product object (1)»

Нет метода __str__. Добавьте его и верните строку.

Изменил модель, а в базе ничего не поменялось

Вы не выполнили makemigrations и migrate, или приложения нет в INSTALLED_APPS.

null=True на ManyToManyField

Параметр не действует, Django выдаст предупреждение fields.W340. Необязательность связи задаётся blank=True.

Изменяемый объект в default

default={} или default=[] делит один объект между всеми экземплярами. Передавайте вызываемый объект: default=dict, default=list.

Два товара с одинаковым названием ломают уникальный slug

slug из title совпадёт, и save() упадёт с IntegrityError. Добавляйте суффикс (ID, счётчик) или делайте slug уникальным в пределах категории через UniqueConstraint.

Частые вопросы

Чем абстрактная модель отличается от proxy-модели?

Абстрактная модель (abstract = True в Meta) своей таблицы не имеет: её поля копируются в таблицу каждого наследника. Так выносят общие поля вроде created_at. Proxy-модель (proxy = True) работает с таблицей родителя и меняет только поведение: сортировку, менеджер, методы, отдельный раздел в админке. Обычное наследование без этих опций создаёт таблицу для каждого класса и связывает их через OneToOneField — при каждом чтении наследника выполняется JOIN.

class TimeStamped(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True


class Product(TimeStamped):   # колонка created_at появится в shop_product
    title = models.CharField(max_length=200)

Как добавить вычисляемое поле?

Если значение нужно только в Python, хватит @property: в базе его нет, фильтровать по нему нельзя. Если по значению нужно фильтровать и сортировать, используйте GeneratedField (Django 5.0+) — база сама считает колонку из других полей:

final_price = models.GeneratedField(
    expression=models.F("price") * (100 - models.F("discount")) / 100,
    output_field=models.DecimalField(max_digits=12, decimal_places=2),
    db_persist=True,
)

Разовое вычисление для конкретного запроса делают через annotate(), см. шпаргалку по QuerySet.

Как сделать свой менеджер, например только опубликованные товары?

Унаследуйтесь от models.Manager и переопределите get_queryset(). Стандартный objects объявите первым: Django использует первый менеджер модели как менеджер по умолчанию.

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status="published")


class Product(models.Model):
    # ...
    objects = models.Manager()
    published = PublishedManager()

# Product.published.all() — только опубликованные

Можно ли сделать первичным ключом UUID?

Да, объявите поле с primary_key=True — автоматический id тогда не создаётся:

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

UUID не раскрывает число записей в URL, но занимает больше места, чем целое, и индекс по случайным значениям растёт быстрее. Если UUID нужен только во внешних ссылках, оставьте целочисленный id и добавьте отдельное поле, как external_id в модели выше.

Как разбить models.py на несколько файлов?

Создайте пакет models/ вместо файла и импортируйте модели в models/__init__.py:

# shop/models/__init__.py
from .category import Category
from .product import Product

Django находит модели, импортированные в модуль models приложения, поэтому makemigrations увидит их без дополнительных настроек.

Итог

Модель — это описание таблицы: поля с параметрами, __str__ для читаемого вывода, Meta для сортировки, названий, индексов и ограничений, TextChoices/IntegerChoices для наборов значений. Переопределяйте save() аккуратно — с super() и учётом update_fields — и помните, что массовые операции его не вызывают.

Дальше: научитесь делать запросы по шпаргалке QuerySet, разберитесь с миграциями и подключите модели к админке Django. Полный справочник полей — в документации Django.

  • #Django
  • #Django ORM
  • #Модели
  • #Meta
  • #choices
↑ ↓ выборEnter открытьEsc закрыть