Django ORM

F expressions в Django ORM | Курс Django ORM урок 3.2

F expressions в Django ORM | Курс Django ORM урок 3.2
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 6

Цель урока

Разобрать F expressions, способ ссылаться на значения полей модели прямо в SQL без загрузки объектов в Python. Понять как F() решает проблему race condition при обновлении счетчиков, позволяет сравнивать поля одной строки между собой и использоваться в аннотациях.

Необходимые знания

  • Урок 2.5: update(), bulk_update()
  • Урок 3.1: Q objects
  • Базовое понимание атомарности операций в SQL

Проблема, которую решает F()

Классическая задача: уменьшить остаток товара на 1 при оформлении заказа.

Наивный подход:

product = Product.objects.get(id=1)
product.stock -= 1
product.save(update_fields=["stock", "updated_at"])

SQL который выполняется:

-- Запрос 1: SELECT
SELECT * FROM shop_product WHERE id = 1;

-- Python: product.stock = 42 - 1 = 41

-- Запрос 2: UPDATE
UPDATE shop_product SET stock = 41, updated_at = NOW() WHERE id = 1;

Проблема, race condition. Если два запроса выполняются одновременно:

Процесс A: SELECT stock = 42
Процесс B: SELECT stock = 42
Процесс A: UPDATE stock = 41  (42 - 1)
Процесс B: UPDATE stock = 41  (42 - 1, но не 40!)

Вместо уменьшения на 2 получаем уменьшение на 1. Один заказ потерян.

F() решает это, перенося вычисление в SQL:

from django.db.models import F

Product.objects.filter(id=1).update(stock=F("stock") - 1)
UPDATE shop_product SET stock = stock - 1 WHERE id = 1;

Один атомарный запрос. PostgreSQL выполняет вычисление с актуальным значением на момент UPDATE, race condition невозможен.


Синтаксис F()

from django.db.models import F

F("field_name")           # ссылка на поле
F("related_field__name")  # через связь (только для filter/annotate, не update)

F() поддерживает арифметику:

F("stock") + 10
F("stock") - 1
F("price") * 2
F("price") / 100
F("price") ** 2       # возведение в степень
F("price") % 3        # остаток от деления

F() в update()

# Увеличить stock для всех продуктов категории
Product.objects.filter(category__slug="phones").update(
    stock=F("stock") + 10
)
UPDATE shop_product
SET stock = stock + 10
WHERE category_id = (SELECT id FROM shop_category WHERE slug = 'phones');
# Применить скидку 15% к цене
Product.objects.filter(is_active=True).update(
    price=F("price") * Decimal("0.85"),
    updated_at=timezone.now(),
)
UPDATE shop_product
SET price = price * 0.85,
    updated_at = '2026-02-15 10:00:00+00'
WHERE is_active = true;

F() в filter() - сравнение полей между собой

F() позволяет сравнивать поля одной строки прямо в WHERE:

# Продукты у которых остаток меньше минимально допустимого
# допустим есть поле min_stock
Product.objects.filter(stock__lt=F("min_stock"))
WHERE stock < min_stock

В нашем проекте OrderItem имеет поля quantity и price. Допустим нужно найти позиции где зафиксированная цена ниже текущей цены продукта (товар подорожал с момента заказа):

# Позиции заказа где зафиксированная цена ниже текущей цены продукта
# товар подорожал с момента заказа
OrderItem.objects.filter(price__lt=F("product__price"))
SELECT shop_orderitem.*
FROM shop_orderitem
INNER JOIN shop_product ON shop_orderitem.product_id = shop_product.id
WHERE shop_orderitem.price < shop_product.price;

Ещё примеры сравнения полей:

# Заказы где дата обновления позже даты создания
# были изменения после создания
Order.objects.filter(updated_at__gt=F("created_at"))

# Продукты где текущий stock равен начальному (условный пример)
Product.objects.filter(stock=F("initial_stock"))

F() с lookups

F() работает со всеми числовыми и временными lookups:

from datetime import timedelta
from django.db.models import F

# Заказы обновленные более чем через час после создания
Order.objects.filter(
    updated_at__gt=F("created_at") + timedelta(hours=1)
)
WHERE updated_at > (created_at + INTERVAL '1 hour')

F() в annotate()

F() часто используется в аннотациях для вычисляемых полей:

from django.db.models import F, ExpressionWrapper, DecimalField

# Добавить вычисляемое поле, итоговая стоимость позиции
OrderItem.objects.annotate(
    subtotal=ExpressionWrapper(
        F("quantity") * F("price"),
        output_field=DecimalField(max_digits=12, decimal_places=2),
    )
)
SELECT shop_orderitem.*,
       (quantity * price) AS subtotal
FROM shop_orderitem;

Django умеет выводить тип для простых арифметических выражений автоматически, поэтому ExpressionWrapper не обязателен:

OrderItem.objects.annotate(subtotal=F("quantity") * F("price"))

ExpressionWrapper с явным output_field нужен в двух случаях: когда Django не может вывести тип самостоятельно (например, арифметика с датами и числами), или когда требуется задать точные параметры поля (max_digits, decimal_places) для корректной агрегации. Для надёжности и совместимости между PostgreSQL и SQLite лучше указывать output_field явно.

Аннотации разберем подробно в уроке 3.3. Здесь важно понять: F() это строительный блок для выражений в annotate().


F() и строковые поля

F() работает и со строками, можно конкатенировать:

from django.db.models.functions import Concat
from django.db.models import Value

# Добавить префикс к slug
Product.objects.update(
    slug=Concat(Value("new-"), F("slug"))
)
UPDATE shop_product SET slug = CONCAT('new-', slug);

Value() это обертка для литеральных значений в выражениях ORM. Без Value() строка "new-" будет интерпретирована как имя поля.


F() после save() - важная деталь

После обновления через F() атрибут объекта в Python содержит не число, а объект CombinedExpression:

product = Product.objects.get(id=1)
product.stock = F("stock") + 1
product.save(update_fields=["stock"])

print(product.stock)  # F(stock) + Value(1) это не число!

Важно понимать, когда вы пишете product.stock = F("stock") + 1, никакого запроса в базу не происходит. Это просто создание "инструкции". Запрос улетает только в момент вызова .save().

Чтобы получить актуальное значение, нужно обновить объект из БД:

product.refresh_from_db()
print(product.stock)  # 43 - реальное значение

refresh_from_db() делает SELECT по всем полям текущего объекта:

SELECT id, name, slug, price, stock, ... FROM shop_product WHERE id = 1;

Можно указать конкретные поля для обновления:

product.refresh_from_db(fields=["stock"])
SELECT stock FROM shop_product WHERE id = 1;

Практические сценарии

Счетчик просмотров

# Атомарно увеличить счетчик просмотров
Product.objects.filter(id=product_id).update(
    views_count=F("views_count") + 1
)

Списание со склада

from django.db.models import F
from django.db import transaction

def place_order(product_id, quantity):
    with transaction.atomic():
        # Проверить наличие и списать атомарно
        updated = Product.objects.filter(
            id=product_id,
            stock__gte=quantity,  # проверка наличия прямо в UPDATE
        ).update(
            stock=F("stock") - quantity
        )

        if updated == 0:
            raise ValueError("Недостаточно товара на складе")
UPDATE shop_product
SET stock = stock - 2
WHERE id = 1 AND stock >= 2;
-- Если stock < 2 - 0 строк обновлено

Это атомарная проверка и списание в одном запросе, race condition невозможен.

Перенос данных между полями

# Скопировать текущую цену в поле "базовой цены"
Product.objects.update(base_price=F("price"))
UPDATE shop_product SET base_price = price;

Сравнение подходов

Подход SQL Race condition Запросов
product.stock -= 1; product.save() SELECT + UPDATE с константой Да 2
Product.objects.filter(id=1).update(stock=F("stock") - 1) UPDATE с вычислением Нет 1

Практическое задание

  1. Напишите функцию restock(category_slug, amount), которая увеличивает stock на amount для всех продуктов указанной категории. Используйте F(). Посмотрите SQL.

  2. Найдите все OrderItem, где зафиксированная цена (price) отличается от текущей цены продукта (product__price). Используйте F() в filter().

  3. Аннотируйте QuerySet OrderItem полем subtotal = quantity * price. Используйте F() и ExpressionWrapper. Посмотрите SQL.

  4. Создайте функцию apply_discount(product_ids, percent), которая применяет скидку к списку продуктов. После обновления выполните refresh_from_db() на одном объекте и убедитесь что атрибут price содержит число, а не выражение.

  5. Реализуйте атомарное списание товара со склада (как в примере выше). Проверьте что при stock=2 и quantity=3 функция поднимает исключение.


Возможные ошибки

Использование F() там, где нужен select_for_update()

F() защищает от race condition при одиночном UPDATE. Но если между проверкой и обновлением есть бизнес-логика в Python, нужна блокировка строки:

# F() не поможет если нужно принять решение в Python
product = Product.objects.get(id=1)
if product.stock > 0:          # проверка в Python
    product.stock -= 1          # между проверкой и обновлением, окно для гонки
    product.save()

# Правильно будет блокировка строки (разберем в уроке 6.2)
with transaction.atomic():
    product = Product.objects.select_for_update().get(id=1)
    if product.stock > 0:
        product.stock -= 1
        product.save()

Забыть refresh_from_db() после save() с F()

product.stock = F("stock") + 1
product.save(update_fields=["stock"])
# product.stock сейчас объект F(), не число

product.refresh_from_db(fields=["stock"])
# теперь product.stock актуальное число из БД

Отсутствие output_field когда тип не выводится автоматически

Django выводит тип сам для простых случаев (int * decimal). Но для арифметики с датами или нестандартных комбинаций без output_field будет ошибка:

from datetime import timedelta

# FieldError: Cannot resolve expression type
Order.objects.annotate(
    processing_time=F("updated_at") - F("created_at")  # тип результата неизвестен
)

# Правильно
from django.db.models import DurationField

Order.objects.annotate(
    processing_time=ExpressionWrapper(
        F("updated_at") - F("created_at"),
        output_field=DurationField(),
    )
)

F() через связь в update()

# Ошибка, нельзя обновить через traversal
Product.objects.update(name=F("category__name"))  # FieldError

# F() через связь к полю связанной модели работает в filter() и annotate()
Product.objects.filter(id__gt=F("category__id"))  # OK

# В Category числовое поле, например max_price = models.DecimalField(default=0)
Product.objects.filter(price__lt=F("category__max_price"))  # OK

Связь со следующим уроком

В уроке 3.3 разбираем annotate() и aggregate(), вычисление агрегатных значений на уровне БД. F() будет активно использоваться внутри аннотаций. Разберем как GROUP BY работает в Django ORM, как фильтровать аннотированные значения и почему Meta.ordering может сломать агрегацию.


<< Урок 3.1

Урок 3.3 >>


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

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

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

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

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

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

Пишите info@aisferaic.ru

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