Поля моделей Django | Курс Django ORM урок 1.1
Цель урока
Разобраться, какие типы полей есть в Django ORM, что каждый из них делает на уровне PostgreSQL и как параметры полей влияют на схему базы данных. Отдельно рассмотрим db_default и GeneratedField, возможности, появившиеся в Django 5.x.
Необходимые знания
- Урок 0 пройден, проект запущен
- Базовое понимание типов данных в SQL (VARCHAR, INTEGER, NUMERIC и т.д.)
Как Django превращает поля в SQL
Каждое поле модели, это не просто атрибут Python. При создании миграции Django транслирует его в тип SQL колонки с набором ограничений. Посмотреть, что именно создается, можно через psql:
docker compose exec db psql -U postgres -d ormcourse -c "\d shop_product"
Вывод:
Table "public.shop_product"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
name | character varying(200) | | not null |
slug | character varying(200) | | not null |
description | text | | not null |
price | numeric(10,2) | | not null |
stock | integer | | not null |
is_active | boolean | | not null |
created_at | timestamp with time zone | | not null |
updated_at | timestamp with time zone | | not null |
category_id | bigint | | not null |
Каждая строка здесь, результат конкретного поля модели. Разберем все типы по группам.
Текстовые поля
CharField
name = models.CharField(max_length=200)
SQL: character varying(200) NOT NULL
max_length, обязательный параметр. Он задает VARCHAR(n) в PostgreSQL. Важно понимать: PostgreSQL не хранит короткие строки эффективнее, чем длинные. VARCHAR(200) и VARCHAR(2000) физически занимают одинаково места при одинаковом содержимом. max_length существует прежде всего как ограничение на уровне приложения и валидации Django.
Когда использовать:
- Короткие строки с известным максимумом (имена, заголовки, slug)
- Когда нужна валидация длины
TextField
description = models.TextField(blank=True)
SQL: text NOT NULL (при blank=True значение по умолчанию, пустая строка)
TextField не имеет ограничения на длину на уровне БД. В PostgreSQL тип text и varchar хранятся одинаково, разница только в наличии/отсутствии ограничения длины. TextField, для произвольных по длине данных: описания, статьи, комментарии.
SlugField
slug = models.SlugField(max_length=200, unique=True)
SQL: character varying(200) NOT NULL + UNIQUE constraint
SlugField это CharField с валидатором на уровне Django (только буквы, цифры, дефис, подчеркивание). На уровне БД отличается только наличием уникального индекса при unique=True.
Числовые поля
IntegerField и его варианты
stock = models.PositiveIntegerField(default=0)
rating = models.PositiveSmallIntegerField()
| Поле Django | SQL тип | Диапазон |
|---|---|---|
SmallIntegerField |
smallint |
-32768 до 32767 |
IntegerField |
integer |
-2.1B до 2.1B |
BigIntegerField |
bigint |
-9.2E18 до 9.2E18 |
PositiveSmallIntegerField |
smallint + CHECK |
0 до 32767 |
PositiveIntegerField |
integer + CHECK |
0 до 2.1B |
PositiveIntegerField добавляет CHECK constraint на уровне БД:
ALTER TABLE shop_product ADD CONSTRAINT shop_product_stock_check CHECK (stock >= 0);
Это означает, что база данных сама откажет в записи отрицательного значения, даже если обойти валидацию Django.
DecimalField
price = models.DecimalField(max_digits=10, decimal_places=2)
SQL: numeric(10,2) NOT NULL
max_digits общее количество цифр, decimal_places цифр после запятой. NUMERIC в PostgreSQL хранит точное значение без потери точности (в отличие от FLOAT). Для денежных значений всегда используйте DecimalField, никогда FloatField.
FloatField → SQL double precision, хранит приближенное значение. Для цен и финансов неприемлемо.
Булевое поле
is_active = models.BooleanField(default=True)
SQL: boolean NOT NULL DEFAULT true
В PostgreSQL, нативный тип boolean.
BigAutoField
Django 3.2+ добавил BigAutoField как дефолтный тип для автоинкрементных первичных ключей (раньше был AutoField / integer). Это важно при создании новых проектов, первичные ключи теперь bigint по умолчанию.
Поля дат и времени
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
SQL: timestamp with time zone NOT NULL
auto_now_add=Trueустанавливает текущее время при создании объекта, поле становится non-editableauto_now=Trueобновляет время при каждомsave(), тоже non-editable
Важно, при auto_now=True Django всегда включает это поле в запрос UPDATE, даже если вы явно его не трогали:
UPDATE shop_product SET name = 'iPhone 16', updated_at = '2026-01-01 12:00:00+00' WHERE id = 1;
Другие варианты:
DateField→dateTimeField→time without time zone
Параметры полей
Параметры определяют поведение поля как на уровне Python, так и на уровне схемы БД.
null и blank
# null=True - разрешает NULL в базе данных
parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL)
# blank=True - разрешает пустое значение в формах Django и валидации
description = models.TextField(blank=True)
Распространенная ошибка, путать null и blank. Это разные уровни:
null=True→NULLразрешен в колонке SQLblank=True→ пустая строка / None проходит валидацию Django
Для текстовых полей (CharField, TextField) принято использовать blank=True без null=True. Так в базе будет пустая строка, а не NULL, это упрощает запросы (не нужно проверять и NULL, и пустую строку).
Для полей IntegerField, DateField, ForeignKey при необходимости опционального значения нужны оба: null=True, blank=True.
default
stock = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=20, choices=Status, default=Status.PENDING)
default значение по умолчанию на уровне Python. Оно применяется в Django при создании объекта, но не попадает в схему БД как SQL DEFAULT. Это означает: если вставить строку напрямую через SQL минуя Django, дефолт не применится.
db_default - дефолты на уровне БД (Django 5.0+)
from django.db.models.functions import Now
class Product(models.Model):
created_at = models.DateTimeField(db_default=Now())
stock = models.PositiveIntegerField(db_default=0)
SQL после миграции:
ALTER TABLE shop_product ADD COLUMN created_at timestamptz NOT NULL DEFAULT now();
ALTER TABLE shop_product ADD COLUMN stock integer NOT NULL DEFAULT 0;
db_default задает DEFAULT непосредственно в DDL. Это важно в двух ситуациях:
-
Добавление NOT NULL колонки в таблицу с данными, без
db_defaultмиграция упадет (PostgreSQL не знает, чем заполнить существующие строки). Сdb_default, применится без проблем. -
Вставка через сторонние инструменты, если данные пишутся не через Django ORM (скрипты, другие сервисы), дефолт все равно применится.
Можно использовать оба параметра вместе:
stock = models.PositiveIntegerField(default=0, db_default=0)
# default=0 - для Django ORM
# db_default=0 - для вставок напрямую в PostgreSQL
unique
slug = models.SlugField(max_length=200, unique=True)
SQL: добавляет UNIQUE constraint и индекс:
ALTER TABLE shop_product ADD CONSTRAINT shop_product_slug_key UNIQUE (slug);
unique=True автоматически создает индекс, дополнительно указывать db_index=True не нужно.
db_index
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
ForeignKey автоматически создает индекс на колонке category_id. Для обычных полей индекс создается через db_index=True:
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
SQL:
CREATE INDEX shop_product_created_at ON shop_product (created_at);
Когда это нужно: если вы часто фильтруете или сортируете по этому полю. Подробнее, в уроке 5.1.
choices
class Order(models.Model):
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PROCESSING = "processing", "Processing"
SHIPPED = "shipped", "Shipped"
status = models.CharField(max_length=20, choices=Status, default=Status.PENDING)
choices не влияет на схему БД, это валидация и отображение на уровне Django. В PostgreSQL колонка будет обычным VARCHAR. Если нужно ограничение на уровне БД, используйте CheckConstraint (урок 6.3).
В Django 5.0 choices принимает не только список кортежей, но и словари и callable:
# Словарь с группировкой
SPORT_CHOICES = {
"Racket sports": {"tennis": "Tennis", "badminton": "Badminton"},
"Water sports": {"swimming": "Swimming", "diving": "Diving"},
}
# Callable - значения вычисляются при каждом обращении
def get_year_choices():
return [(y, str(y)) for y in range(2020, 2030)]
year = models.IntegerField(choices=get_year_choices)
GeneratedField - вычисляемые колонки (Django 5.0+)
GeneratedField это поле, значение которого всегда вычисляется базой данных на основе других полей. Django не записывает в него данные, это делает PostgreSQL.
from django.db.models import F, GeneratedField, ExpressionWrapper, fields
class OrderItem(models.Model):
quantity = models.PositiveIntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)
# Вычисляемое поле: quantity * price
subtotal = GeneratedField(
expression=ExpressionWrapper(
F("quantity") * F("price"),
output_field=fields.DecimalField(max_digits=12, decimal_places=2),
),
output_field=fields.DecimalField(max_digits=12, decimal_places=2),
db_persist=True, # хранить в БД, не вычислять на лету
)
SQL, который создает миграция:
ALTER TABLE shop_orderitem ADD COLUMN subtotal numeric(12,2)
GENERATED ALWAYS AS (quantity * price) STORED;
db_persist=True → STORED, значение хранится физически и обновляется при каждом INSERT/UPDATE. db_persist=False → VIRTUAL, вычисляется при каждом SELECT (PostgreSQL поддерживает только STORED).
Что важно понимать:
- Поле read-only, попытка записать в него вызовет ошибку
- При SELECT Django включает его как обычную колонку
- Обновляется автоматически при изменении
quantityилиprice - В Django 6.0
GeneratedFieldобновляется из БД послеsave()в бэкендах с поддержкой RETURNING (PostgreSQL, SQLite, Oracle), не нужно делатьrefresh_from_db()
Когда это полезно, когда поле вычисляется из других полей этой же строки, и вам нужно фильтровать или сортировать по нему (индекс на GeneratedField работает).
Когда не подходит: если вычисление требует данных из других таблиц, для этого нужны аннотации (урок 3.3).
Специальные поля
AutoField / BigAutoField
# В settings.py
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Начиная с Django 3.2, BigAutoField стал дефолтным (было AutoField). Это означает, что id создается как bigint, а не integer, integer ограничен ~2.1 миллиарда строк.
Если явно не указать primary_key=True ни для одного поля, Django автоматически добавит:
id = models.BigAutoField(primary_key=True)
SQL:
id bigint NOT NULL DEFAULT nextval('shop_product_id_seq')
UUIDField
import uuid
from django.db import models
class Product(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
SQL: uuid NOT NULL DEFAULT ...
PostgreSQL имеет нативный тип uuid. Использование UUID как первичного ключа, компромисс: глобальная уникальность без координации, но случайные UUID фрагментируют B-tree индекс. Если нужны UUID, рассмотрите UUID v7, они лишены этой проблемы.
JSONField
metadata = models.JSONField(default=dict, blank=True)
SQL: jsonb NOT NULL DEFAULT '{}'
Django использует PostgreSQL тип jsonb (бинарный JSON с индексированием). Поддерживает фильтрацию через lookups:
Product.objects.filter(metadata__color="red")
Product.objects.filter(metadata__sizes__contains=["M", "L"])
Подробно, в уроке 7.2.
CompositePrimaryKey - составной первичный ключ (Django 5.2+)
CompositePrimaryKey позволяет объявить PK из нескольких полей. Это соответствует реляционной практике для связующих таблиц и таблиц с составной бизнес-идентичностью:
from django.db.models import CompositePrimaryKey
class OrderItem(models.Model):
pk = CompositePrimaryKey("order_id", "product_id")
order = models.ForeignKey("Order", on_delete=models.CASCADE)
product = models.ForeignKey("Product", on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)
CREATE TABLE shop_orderitem (
order_id BIGINT NOT NULL REFERENCES shop_order(id),
product_id BIGINT NOT NULL REFERENCES shop_product(id),
quantity INTEGER NOT NULL DEFAULT 1,
price NUMERIC(10, 2) NOT NULL,
PRIMARY KEY (order_id, product_id)
-- нет отдельного поля id
);
Преимущества:
- Нет лишнего поля
id, экономия места и один индекс вместо двух - Составной PK уже гарантирует уникальность пары (order, product), не нужен отдельный
UniqueConstraint
Ограничения:
pk, виртуальное поле, не колонка в БД.obj.pkвозвращает кортеж:(order_id, product_id)get(pk=(1, 5)), синтаксис с кортежем- ForeignKey на модель с CompositePrimaryKey требует явного
to_fields
# Создание
item = OrderItem.objects.create(order_id=1, product_id=5, quantity=2, price=Decimal("99.00"))
# Получение
item = OrderItem.objects.get(pk=(1, 5))
# или
item = OrderItem.objects.get(order_id=1, product_id=5)
print(item.pk) # (1, 5)
Когда использовать: для связующих таблиц M2M где нет дополнительных атрибутов, или для таблиц где пара полей является естественным ключом. Если в связующей таблице есть дополнительные поля (дата, количество), можно использовать и с CompositePrimaryKey, как показано выше.
Практическое задание
Возьми модели из урока 0 и ответь на следующие вопросы через psql (\d table_name) и анализ кода:
- Какой тип SQL у поля
ratingв моделиReview? Есть ли на нем CHECK constraint? - Почему у
Order.total_priceестьdefault=0, но нетnull=True? - Добавьте в модель
Productполеdiscount_priceтипаDecimalField, которое может быть пустым (товар без скидки). Какие параметры нужны? - Добавьте в
OrderItemвычисляемое полеsubtotalчерезGeneratedField. Сделай миграцию и проверьте схему через\d shop_orderitem.
Возможные ошибки
null=True у CharField/TextField
# Плохо, два варианта "пусто": NULL и ""
name = models.CharField(max_length=200, null=True, blank=True)
# Хорошо, только пустая строка
name = models.CharField(max_length=200, blank=True)
FloatField для денег
# Плохо, потеря точности
price = models.FloatField()
# Хорошо
price = models.DecimalField(max_digits=10, decimal_places=2)
Забыть db_default при добавлении NOT NULL колонки
# Миграция упадет если в таблице уже есть данные:
new_field = models.IntegerField()
# Правильно:
new_field = models.IntegerField(db_default=0)
# или:
new_field = models.IntegerField(null=True) # потом отдельной миграцией убрать null
Ненужный db_index на ForeignKey
# ForeignKey уже создает индекс
category = models.ForeignKey(Category, on_delete=models.PROTECT, db_index=True)
Связь со следующим уроком
В следующем уроке разберем связи между моделями: ForeignKey, OneToOneField, ManyToManyField. Посмотрим, что Django создает в PostgreSQL для каждого типа связи: внешние ключи, таблицы join, индексы. И разберем параметры, которые влияют на поведение при удалении связанных объектов.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru