Класс F, Value и метод annotate()

Курс по Django: https://stepik.org/a/183363

В предыдущих примерах мы делали выборки, указывая конкретные значения полей, например, так:

Women.objects.filter(pk__lte=2)

Но что если вместо 2 нужно прописать значение другого поля таблицы? Просто указать его не получится, например, такая запись:

Women.objects.filter(pk__gt="cat_id")

приведет к ошибке. Для этого нужно использовать специальный класс F, позволяющий нам выполнять подобные операции. Расположен он в ветке django.db.models:

from django.db.models import F

И, далее, строку "cat_id" обернем в класс F:

Women.objects.filter(pk__gt=F("cat_id"))

Получим все записи, кроме первой (с id=1). А SQL-запрос будет иметь вид:

SELECT … FROM "women_women" WHERE "women_women"."id" > "women_women"."cat_id"

Здесь условие «id > cat_id» как раз и было сформировано благодаря использованию F класса.

Конечно, это искусственный пример, демонстрирующий работу F-класса. Часто подобные операции приходится делать, когда нужно увеличить, например, счетчик просмотра страниц. Давайте для примера в модель Husband добавим счетчик числа женитьб для мужчин:

class Husband(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField(null=True)
    m_count = models.IntegerField(blank=True, default=0)
 
    def __str__(self):
        return self.name

Создадим и выполним миграции:

python manage.py makemigrations   
python manage.py migrate

Снова перейдем в консоль фреймворка Django:

python manage.py shell_plus --print-sql

И выполним команду:

Husband.objects.update(m_count=F("m_count")+1)

Ей соответствует следующий SQL-запрос:

UPDATE "women_husband" SET "m_count" = ("women_husband"."m_count" + 1)

Мы здесь перебираем все записи таблицы husband и для каждой из них увеличиваем значение поля m_count на единицу. В результате все нули стали единицей. Или можно сделать так. Сначала получить объект записи:

h = Husband.objects.get(pk=1)

А, затем, изменить значение поля:

h.m_count = F("m_count") + 1
h.save()

Теперь счетчик женитьб Брэда Питта стал равен двум.

У вас здесь может возникнуть вопрос: а почему бы нам в данном случае не использовать операцию инкремента:

h.m_count += 1

В результате у меня сформировался следующий SQL-запрос:

UPDATE "women_husband" SET "name" = 'Брэд Питт', "age" = 30,      "m_count" = (("women_husband"."m_count" + 1) + 1) WHERE "women_husband"."id" = 1

Как вы понимаете, это не то, что мы бы хотели видеть. Здесь произошло увеличение сразу на два.

Вообще по документации ORM Django такой подход не рекомендуется. Здесь могут возникать неопределенности при одновременном изменении поля одной и той же записи разными пользователями. Тогда значение m_count будет неопределенным. Класс F решает подобные коллизии.

Метод annotate()

При формировании выборки записей из таблиц БД ORM Django предоставляет возможность формировать дополнительные вычисляемые поля. Что это такое? Давайте предположим, что мы бы хотели автоматически формировать новое булево поле is_married для таблицы husband, со значениями True (истина). Для этого следует воспользоваться специальным методом, который называется annotate(), следующим образом:

from django.db.models import Value
lst = Husband.objects.all().annotate(is_married=Value(True))

Здесь Value – это специальный класс, который используется для формирования вычисляемых значений полей таблицы. В данном случае мы просто указали константную величину True.

Давайте теперь выведем записи в табличном виде следующим образом:

for i, x in enumerate(lst):
     if i == 0:
         print(list(x.__dict__)[1:])
     print(list(x.__dict__.values())[1:])

Увидим информацию в таком виде:

['id', 'name', 'age', 'm_count', 'is_married']
[1, 'Брэд Питт', 30, 4, True]
[2, 'Том Акерли', 31, 1, True]
[3, 'Дэниэл Модер', 54, 1, True]
[4, 'Кук Марони', 37, 1, True]
[5, 'Сергей Балакирев', 101, 1, True]

Обратите внимание на последнее поле is_married, которого нет в таблице husband, но появилось в нашей выборке. Это и есть результат работы метода annotate().

Внутри класса Value мы можем записывать любые вычисляемые значения. Например, такие:

lst = Husband.objects.all().annotate(is_married=Value(2 + 5))
lst = Husband.objects.all().annotate(is_married=Value("hi "*3))

И так далее. Однако не можем использовать значения полей. Если записать что то вроде:

lst = Husband.objects.all().annotate(is_married=Value("m_count"*3))

то получим строку в новом поле is_married.

Чтобы оперировать полем в методе annotate() следует использовать только что рассмотренный класс F, например, так (у некоторых мужчин m_count приравнены нулю):

lst = Husband.objects.all().annotate(is_married=F("m_count"))

Тогда при выводе увидим:

['id', 'name', 'age', 'm_count', 'is_married']
[1, 'Брэд Питт', 30, 4, 4]
[2, 'Том Акерли', 31, 1, 1]
[3, 'Дэниэл Модер', 54, 0, 0]
[4, 'Кук Марони', 37, 1, 1]
[5, 'Сергей Балакирев', 101, 0, 0]

Поле is_married стало принимать те же значения, что и поле m_count. Конечно, от такого действия особого смысла нет. Часто новые поля содержат результаты вычислений на основе предыдущих полей.  Например, можно условно вычислить стаж работы, основываясь на возрасте:

lst = Husband.objects.all().annotate(work_age=F("age") - 20)

(Мы здесь опять же условно полагаем, что работать начинают в 20 лет.). Получим результаты:

['id', 'name', 'age', 'm_count', 'work_age']
[1, 'Брэд Питт', 30, 4, 10]
[2, 'Том Акерли', 31, 1, 11]
[3, 'Дэниэл Модер', 54, 0, 34]
[4, 'Кук Марони', 37, 1, 17]
[5, 'Сергей Балакирев', 101, 0, 81]

Или можно сделать так:

lst = Husband.objects.all().annotate(work_age=F("age") - 20, salary=F("age") * 1.10)

Сформировать сразу два новых поля work_age и salary на основе существующего поля age. Наконец, можно оперировать сразу несколькими ранее определенными полями, например:

lst = Husband.objects.all().annotate(salary=F("age") * 1.10 - F("m_count") * 5)

На этом мы завершим текущее занятие, а на следующем продолжим знакомиться с возможностями ORM Django.

Курс по Django: https://stepik.org/a/183363

Видео по теме