Модели в Django: поля, Meta, __str__ и choices
Как описывать модели Django: типы полей и их параметры, __str__, класс Meta, choices через TextChoices и IntegerChoices, переопределение save() и миграции.
Содержание · 13 разделов
- Первая модель
- Типы полей Django
- IntegerField и его варианты
- Параметры полей
- Метод __str__
- Класс Meta
- Choices: TextChoices и IntegerChoices
- Метод save() и его переопределение
- Связи между моделями
- values(): данные модели без объектов
- Миграции
- Частые ошибки
- TypeError: missing 1 required positional argument: ‘on_delete’
- Объекты отображаются как «Product object (1)»
- Изменил модель, а в базе ничего не поменялось
- null=True на ManyToManyField
- Изменяемый объект в default
- Два товара с одинаковым названием ломают уникальный slug
- Частые вопросы
- Чем абстрактная модель отличается от proxy-модели?
- Как добавить вычисляемое поле?
- Как сделать свой менеджер, например только опубликованные товары?
- Можно ли сделать первичным ключом UUID?
- Как разбить models.py на несколько файлов?
- Итог
Модель 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 | число с плавающей точкой | — |
BooleanField | True/False | — |
DateField, DateTimeField, TimeField | дата, дата и время, время | — |
EmailField, URLField, SlugField | строки с валидацией формата | — |
UUIDField | UUID | — |
JSONField | JSON (словари, списки) | — |
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