В прошлых уроках мы научились извлекать записи, выполнять фильтрацию и сортировку, использовать first(), last(), count(), exists().
Теперь мы поднимаемся на следующий уровень.
Сегодня мы разберём три мощных инструмента ORM:
F— работа с полями самой модели в математических выраженияхValue— добавление статических значенийannotate()— создание вычисляемых полей «на лету»
Эти инструменты позволяют переносить вычисления в базу данных, снижать нагрузку на Python и сокращать количество SQL-запросов.
Представьте ситуацию:
У фильма есть поле rating — допустим, текущий рейтинг 8.2.
Мы хотим увеличить рейтинг каждого фильма на 0.1, например, после массовой обработки отзывов.
Можно написать:
movie.rating = movie.rating + 0.1
movie.save()Но если в этот момент два запроса обновляют рейтинг одновременно, один из них затрет изменения другого. Или окажется, что в памяти объекта старое значение, которое не соответствует данным в базе.
Чтобы избежать подобных коллизий, Django использует класс F, который позволяет выполнять операции не в Python, а прямо в SQL.
Мы используем модели:
MovieDirectorGenreReview
Да, такое иногда бывает 🙂 Например, фильм снят за копейки, но стал хитом.
from django.db.models import F
Movie.objects.filter(rating__gt=F("budget") / 1_000_000)Django преобразует выражение к SQL:
WHERE rating > (budget / 1000000)Если поле budget типа IntegerField, а деление даёт float, некоторые базы данных могут округлять результаты.
Это нормально, но студент должен понимать, что:
- PostgreSQL делит точно
- SQLite — иногда приводит к integer division
Movie.objects.update(rating=F("rating") + 0.1)SQL:
UPDATE movies_movie SET rating = rating + 0.1;Открываем Django shell:
from movies.models import Movie
Movie.objects.values("title", "rating")В браузере:
- создаём простой template-вью (например,
/movies/) - выводим список фильмов
- обновляем страницу и видим новые значения рейтингов
Movie.objects.filter(is_published=True).update(budget=F("budget") + 5_000_000)m = Movie.objects.get(pk=5)
m.rating = F("rating") + 1
m.save()Важно: после сохранения объект
mв памяти не содержит обновлённого значения, а хранитF("rating") + 1.
Чтобы получить новое значение:
m.refresh_from_db()Value используется, когда нужно добавить в запрос константу, которую не нужно хранить в базе.
Например, мы хотим пометить в выдаче все фильмы как:
- актуальные
- популярные
- проверенные
- принадлежащие специальной подборке
Это делается без изменений в базе данных.
from django.db.models import Value
movies = Movie.objects.annotate(is_popular=Value(True))Теперь в каждом объекте:
m.is_popular # TrueПоле не существует в модели, но появляется в результатах query.
annotate() позволяет «добавить» вычисляемые поля к каждому объекту выборки.
Это особенно полезно:
- когда мы вычисляем что-то на основе полей модели
- когда нам нужно что-то связанное (агрегации)
- когда мы формируем статистику
from django.db.models import Count
movies = Movie.objects.annotate(review_count=Count("reviews"))Теперь:
for m in movies:
print(m.title, m.review_count)Теперь самое интересное.
from django.db.models import F
movies = Movie.objects.annotate(
age=2025 - F("year")
)Вывод:
"Silent Dream" — age=6
"Shadow River" — age=2
Допустим, мы хотим вычислить условный «индекс качества»:
quality = rating * 10 – (age * 0.2)
from django.db.models import F
movies = Movie.objects.annotate(
quality=F("rating") * 10 - (2025 - F("year")) * 0.2
)movies = Movie.objects.annotate(
director_age=F("year") - F("director__birth_year")
)Теперь можно отфильтровать:
movies.filter(director_age__lt=30)movies = Movie.objects.annotate(
age=2025 - F("year"),
doubled_rating=F("rating") * 2,
expensive=F("budget") > 100_000_000,
)m.rating = F("rating") + 1
m.save()
print(m.rating) # Выводит F("rating") + 1, а не число!Решение:
m.refresh_from_db()Например:
Movie.objects.update(is_published=F("rating"))is_published — Boolean
rating — Float
DB выдаст ошибку преобразования.
x = 10
Movie.objects.update(rating=F("rating") + x)Это работает нормально, но:
- если
x= float для SQLite может быть округление - если
x= Decimal — возможен конфликт типов
Пока всё просто:
- создаём вью типа:
def debug_movies(request):
movies = Movie.objects.all().annotate(...)
return render(request, "debug_movies.html", {"movies": movies})- в
debug_movies.htmlвыводим:
{% for m in movies %}
<p>{{ m.title }} — {{ m.quality }} ({{ m.director_age }})</p>
{% endfor %}- обновляем страницу после каждого изменения ORM
- фиксируем разницу
Это тренирует:
- понимание запросов
- реакцию в реальном интерфейсе
- способность отлаживать ORM
Вывести список всех фильмов и добавить 虚 турное поле budget_in_millions, где бюджет делится на миллион.
Аннотировать фильмы полем age, показывающим сколько лет прошло с момента выхода фильма.
Вывести фильмы, у которых:
rating * 10 > budget / 1_000_000
Добавить каждому фильму поле review_count — количество отзывов.
Увеличить рейтинг всех фильмов, у которых есть отзывы, на 0.2.
Сформировать список фильмов, у которых режиссёру было менее 35 лет на момент выхода.
Добавить виртуальное поле is_old = True, если фильм старше 15 лет (через F + annotate + Case/When) — если проходили Case/When или использовать Value(True) для упрощённой версии.
Увеличить бюджет всех непубликуемых фильмов на 1 млн с помощью F.
- Для чего используется класс F?
- Что происходит, если сохранить объект с F-выражением без
refresh_from_db()? - Можно ли использовать F для сравнения двух полей внутри одного фильтра?
- Что делает класс Value?
- Можно ли добавлять вычисляемые поля без их сохранения в БД?
- Что делает метод annotate()?
- Чем annotate() отличается от aggregate()?
- Можно ли использовать F и Value внутри одного annotate()?
- Какие типичные ошибки возникают при работе с F?
- Почему annotate() выполняется в SQL, а не в Python?