Django ORM

Поля моделей Django | Курс Django ORM урок 1.1

Поля моделей Django | Курс Django ORM урок 1.1
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 16

Цель урока

Разобраться, какие типы полей есть в 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-editable
  • auto_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;

Другие варианты:

  • DateFielddate
  • TimeFieldtime 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=TrueNULL разрешен в колонке SQL
  • blank=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. Это важно в двух ситуациях:

  1. Добавление NOT NULL колонки в таблицу с данными, без db_default миграция упадет (PostgreSQL не знает, чем заполнить существующие строки). С db_default, применится без проблем.

  2. Вставка через сторонние инструменты, если данные пишутся не через 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=TrueSTORED, значение хранится физически и обновляется при каждом INSERT/UPDATE. db_persist=FalseVIRTUAL, вычисляется при каждом 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) и анализ кода:

  1. Какой тип SQL у поля rating в модели Review? Есть ли на нем CHECK constraint?
  2. Почему у Order.total_price есть default=0, но нет null=True?
  3. Добавьте в модель Product поле discount_price типа DecimalField, которое может быть пустым (товар без скидки). Какие параметры нужны?
  4. Добавьте в 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, индексы. И разберем параметры, которые влияют на поведение при удалении связанных объектов.


<< Урок 0

Урок 1.2 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы