Skip to content

Latest commit

 

History

History
401 lines (253 loc) · 11.2 KB

File metadata and controls

401 lines (253 loc) · 11.2 KB

Модуль 5. Урок 29. Класс F, Value и метод annotate() в Django ORM

В прошлых уроках мы научились извлекать записи, выполнять фильтрацию и сортировку, использовать first(), last(), count(), exists(). Теперь мы поднимаемся на следующий уровень.

Сегодня мы разберём три мощных инструмента ORM:

  • F — работа с полями самой модели в математических выражениях
  • Value — добавление статических значений
  • annotate() — создание вычисляемых полей «на лету»

Эти инструменты позволяют переносить вычисления в базу данных, снижать нагрузку на Python и сокращать количество SQL-запросов.


1. Зачем нужен класс F?

Представьте ситуацию:

У фильма есть поле rating — допустим, текущий рейтинг 8.2. Мы хотим увеличить рейтинг каждого фильма на 0.1, например, после массовой обработки отзывов.

Можно написать:

movie.rating = movie.rating + 0.1
movie.save()

Но если в этот момент два запроса обновляют рейтинг одновременно, один из них затрет изменения другого. Или окажется, что в памяти объекта старое значение, которое не соответствует данным в базе.

Чтобы избежать подобных коллизий, Django использует класс F, который позволяет выполнять операции не в Python, а прямо в SQL.


2. Примеры работы с F в проекте cinemahub

Мы используем модели:

  • Movie
  • Director
  • Genre
  • Review

Пример 1: Фильмы, у которых rating выше budget / 1_000_000

Да, такое иногда бывает 🙂 Например, фильм снят за копейки, но стал хитом.

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

Пример 2: Массовое обновление рейтинга всех фильмов

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/)
  • выводим список фильмов
  • обновляем страницу и видим новые значения рейтингов

Пример 3: Увеличиваем бюджет только опубликованных фильмов

Movie.objects.filter(is_published=True).update(budget=F("budget") + 5_000_000)

3. F в индивидуальном обновлении

m = Movie.objects.get(pk=5)
m.rating = F("rating") + 1
m.save()

Важно: после сохранения объект m в памяти не содержит обновлённого значения, а хранит F("rating") + 1.

Чтобы получить новое значение:

m.refresh_from_db()

4. Зачем нужен Value?

Value используется, когда нужно добавить в запрос константу, которую не нужно хранить в базе.

Например, мы хотим пометить в выдаче все фильмы как:

  • актуальные
  • популярные
  • проверенные
  • принадлежащие специальной подборке

Это делается без изменений в базе данных.


Пример 4: Добавим виртуальное поле is_popular = True

from django.db.models import Value

movies = Movie.objects.annotate(is_popular=Value(True))

Теперь в каждом объекте:

m.is_popular  # True

Поле не существует в модели, но появляется в результатах query.


5. Использование annotate()

annotate() позволяет «добавить» вычисляемые поля к каждому объекту выборки.

Это особенно полезно:

  • когда мы вычисляем что-то на основе полей модели
  • когда нам нужно что-то связанное (агрегации)
  • когда мы формируем статистику

Пример 5: Количество отзывов на фильм

from django.db.models import Count

movies = Movie.objects.annotate(review_count=Count("reviews"))

Теперь:

for m in movies:
    print(m.title, m.review_count)

6. Использование F внутри annotate()

Теперь самое интересное.

Пример 6: Добавляем поле «старость фильма» — age

from django.db.models import F

movies = Movie.objects.annotate(
    age=2025 - F("year")
)

Вывод:

"Silent Dream" — age=6
"Shadow River" — age=2

Пример 7: Суммарный «вес» фильма по формуле

Допустим, мы хотим вычислить условный «индекс качества»:

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
)

Пример 8: Сколько лет было режиссёру, когда он выпустил фильм

movies = Movie.objects.annotate(
    director_age=F("year") - F("director__birth_year")
)

Теперь можно отфильтровать:

movies.filter(director_age__lt=30)

7. Несколько аннотаций за один запрос

movies = Movie.objects.annotate(
    age=2025 - F("year"),
    doubled_rating=F("rating") * 2,
    expensive=F("budget") > 100_000_000,
)

8. Типичные ошибки и как их избежать

Ошибка: используете F без refresh_from_db()

m.rating = F("rating") + 1
m.save()
print(m.rating)  # Выводит F("rating") + 1, а не число!

Решение:

m.refresh_from_db()

Ошибка: используете F с incompatible полями

Например:

Movie.objects.update(is_published=F("rating"))

is_published — Boolean rating — Float DB выдаст ошибку преобразования.


Ошибка: mathematical F зависит от Python-переменной

x = 10
Movie.objects.update(rating=F("rating") + x)

Это работает нормально, но:

  • если x = float для SQLite может быть округление
  • если x = Decimal — возможен конфликт типов

9. Проверка результатов в браузере

Пока всё просто:

  1. создаём вью типа:
def debug_movies(request):
    movies = Movie.objects.all().annotate(...)
    return render(request, "debug_movies.html", {"movies": movies})
  1. в debug_movies.html выводим:
{% for m in movies %}
<p>{{ m.title }} — {{ m.quality }} ({{ m.director_age }})</p>
{% endfor %}
  1. обновляем страницу после каждого изменения ORM
  2. фиксируем разницу

Это тренирует:

  • понимание запросов
  • реакцию в реальном интерфейсе
  • способность отлаживать ORM

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

Задание 1.

Вывести список всех фильмов и добавить 虚 турное поле budget_in_millions, где бюджет делится на миллион.


Задание 2.

Аннотировать фильмы полем age, показывающим сколько лет прошло с момента выхода фильма.


Задание 3.

Вывести фильмы, у которых:

rating * 10 > budget / 1_000_000


Задание 4.

Добавить каждому фильму поле review_count — количество отзывов.


Задание 5.

Увеличить рейтинг всех фильмов, у которых есть отзывы, на 0.2.


Задание 6.

Сформировать список фильмов, у которых режиссёру было менее 35 лет на момент выхода.


Задание 7.

Добавить виртуальное поле is_old = True, если фильм старше 15 лет (через F + annotate + Case/When) — если проходили Case/When или использовать Value(True) для упрощённой версии.


Задание 8.

Увеличить бюджет всех непубликуемых фильмов на 1 млн с помощью F.


Вопросы

  1. Для чего используется класс F?
  2. Что происходит, если сохранить объект с F-выражением без refresh_from_db()?
  3. Можно ли использовать F для сравнения двух полей внутри одного фильтра?
  4. Что делает класс Value?
  5. Можно ли добавлять вычисляемые поля без их сохранения в БД?
  6. Что делает метод annotate()?
  7. Чем annotate() отличается от aggregate()?
  8. Можно ли использовать F и Value внутри одного annotate()?
  9. Какие типичные ошибки возникают при работе с F?
  10. Почему annotate() выполняется в SQL, а не в Python?

Предыдущий урок | Следующий урок