Как стать автором
Обновить
599.13
OTUS
Цифровые навыки от ведущих экспертов

F(), Func() и никаких циклов: как Django думает в SQL

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров3.5K

Привет, Хабр!

Сегодня рассмотрим, как использовать F()‑экспрессии и Func()‑обёртки в Django для того, чтобы выполнять арифметику, условия и преобразования не в Python, а на стороне базы данных. Это важно, когда речь идёт о:

  • массовых обновлениях без save(),

  • атомарности,

  • производительности на высоких нагрузках,

  • и просто — умении думать в терминах SQL, не теряя удобства Django ORM.

F(): когда требуется значение из другого поля

Классическая ситуация — увеличить поле views:

article = Article.objects.get(id=1)
article.views += 1
article.save()

Проблема: между get() и save() может произойти гонка данных. Если несколько воркеров обновляют значение одновременно — часть просмотров потеряется.

Атомарный способ:

from django.db.models import F

Article.objects.filter(id=1).update(views=F('views') + 1)

Это превращается в SQL вида UPDATE article SET views = views + 1 WHERE id = 1. Никакой гонки. Никакой нагрузки на память.

Сравнение между полями

Фильтруем товары, у которых цена выше скидки:

Product.objects.filter(price__gt=F('discount_price'))

База сравнивает значения прямо внутри себя. Нет смысла загружать данные в Python, перебирать, сравнивать вручную. Работает с любыми типами: числа, даты, булевы, строки.

Вычисляемые поля через annotate()

Пусть нужно в выборке вывести стоимость товаров на складе:

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

Product.objects.annotate(
    stock_value=ExpressionWrapper(
        F('price') * F('stock'),
        output_field=DecimalField()
    )
)

Если используете DRF, stock_value будет доступен в сериализаторе как поле модели — без дополнительного кода.

Условные выражения: Case / When

Допустим, если stock == 0, хотим аннотировать строку "Нет в наличии", иначе "В наличии":

from django.db.models import Case, When, Value, CharField

Product.objects.annotate(
    availability=Case(
        When(stock=0, then=Value("Нет в наличии")),
        default=Value("В наличии"),
        output_field=CharField()
    )
)

Полезно, когда хочется передать бизнес‑логику прямо в сериализатор, без дополнительной логики в Python.

Массовые обновления

В продакшене часто встречается кейс: если progress_percent >= 100, то status = 'completed'.

Вариант, который лучше забыть:

for task in Task.objects.all():
    if task.progress_percent >= 100:
        task.status = 'completed'
        task.save()

Так делать нельзя: слишком много SQL‑запросов, лишняя нагрузка, долгие миграции.

Правильный подход:

Task.objects.filter(progress_percent__gte=100).update(status='completed')

SQL-функции через Func()

Хочется аннотировать имя пользователя в верхнем регистре? Без проблем:

from django.db.models import Func, F

User.objects.annotate(name_upper=Func(F('name'), function='UPPER'))

А если требуется собрать full_name:

from django.db.models import Func, F, Value

User.objects.annotate(
    full_name=Func(
        F('first_name'),
        Value(' '),
        F('last_name'),
        function='CONCAT'
    )
)

Всё выполняется на уровне SQL. Никаких .join() в Python, никакой логики в сериализаторах.

Автообновление updated_at через Now()

Когда нужно обновить updated_at, но не хочется вычислять timezone.now() в Python:

from django.db.models.functions import Now

MyModel.objects.filter(condition=True).update(updated_at=Now())

Это NOW() в SQL. Удобно, особенно если работаете с select_for_update и хотите сохранить согласованность.

Собственные SQL-функции на Func()

Django позволяет кастомизировать Func, если нужен нестандартный SQL:

from django.db.models import Func, IntegerField

class ExtractMonth(Func):
    function = 'EXTRACT'
    template = "%(function)s(MONTH FROM %(expressions)s)"
    output_field = IntegerField()

# Использование:
Invoice.objects.annotate(month=ExtractMonth('created_at'))

Работает на PostgreSQL, MySQL, Oracle — везде, где поддерживается EXTRACT.

Возможные проблемки

1. Без output_field в ExpressionWrapper — получите ошибку.

Django не умеет автоматически определять тип, если это не annotate(F() + 1). Поэтому:

ExpressionWrapper(F('a') * F('b'), output_field=DecimalField())

обязателен.

2. F() не сериализуется.

В DRF попытка передать F('views') + 1 в сериализатор вызовет ошибку. Это выражение, а не значение. Использовать можно только в .filter(), .update(), .annotate().

3. SQL‑функции зависят от базы.

Некоторые работают в PostgreSQL, но не в SQLite. Если проект использует SQLite в тестах — проверяйте поддержку.

Кейс

В CRM‑системе необходимо ежедневно пересчитывать KPI менеджеров по заказам:

  • score = orders_count 2 + late_orders -3 + F('bonus')

  • status = "top" если score > 50, иначе "regular"

Решение:

Manager.objects.annotate(
    score=ExpressionWrapper(
        F('orders_count') * 2 + F('late_orders') * -3 + F('bonus'),
        output_field=IntegerField()
    )
).update(
    status=Case(
        When(score__gt=50, then=Value('top')),
        default=Value('regular'),
        output_field=CharField()
    )
)

Весь пересчёт — одним SQL‑запросом. Без фоновых задач. Без утечек.

Использование с select_for_update, Window, Subquery

F() внутри транзакции:

with transaction.atomic():
    obj = Product.objects.select_for_update().get(id=1)
    obj.stock = F('stock') - 1
    obj.save()

F() + Window‑функции:

from django.db.models import Window
from django.db.models.functions import Rank

Leaderboard.objects.annotate(
    rank=Window(
        expression=Rank(),
        order_by=F('score').desc()
    )
)

F() + Subquery:

from django.db.models import Subquery, OuterRef

last_price = PriceHistory.objects.filter(
    product_id=OuterRef('pk')
).order_by('-date').values('price')[:1]

Product.objects.annotate(last_price=Subquery(last_price))

🧾 Итого

Зачем нужны F() и Func():

  • Атомарность. Один SQL — одно действие. Без гонок.

  • Производительность. Работает там, где живут данные.

  • Выразительность. Пишется читаемо, читается понятно.

  • Безопасность. Исключает дублирование и сбои при массовых обновлениях.

  • Расширяемость. Поддерживает кастомные функции, Case, Window, Subquery.


Не пропустите:

23 апреля, 20:00
«Асинхронное Django приложение: работа с асинхронными ORM и views»

Этот урок откроет новые горизонты для оптимизации асинхронных операций в Django. Подробности и регистрация на странице курса «Django‑разработчик».

Теги:
Хабы:
+12
Комментарии3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS