F expressions в Django ORM | Курс Django ORM урок 3.2
Цель урока
Разобрать F expressions, способ ссылаться на значения полей модели прямо в SQL без загрузки объектов в Python. Понять как F() решает проблему race condition при обновлении счетчиков, позволяет сравнивать поля одной строки между собой и использоваться в аннотациях.
Необходимые знания
Проблема, которую решает 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 |
Практическое задание
-
Напишите функцию
restock(category_slug, amount), которая увеличиваетstockнаamountдля всех продуктов указанной категории. ИспользуйтеF(). Посмотрите SQL. -
Найдите все
OrderItem, где зафиксированная цена (price) отличается от текущей цены продукта (product__price). ИспользуйтеF()вfilter(). -
Аннотируйте QuerySet
OrderItemполемsubtotal = quantity * price. ИспользуйтеF()иExpressionWrapper. Посмотрите SQL. -
Создайте функцию
apply_discount(product_ids, percent), которая применяет скидку к списку продуктов. После обновления выполнитеrefresh_from_db()на одном объекте и убедитесь что атрибутpriceсодержит число, а не выражение. -
Реализуйте атомарное списание товара со склада (как в примере выше). Проверьте что при
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 может сломать агрегацию.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru