Группировка записей. Вычисления на стороне СУБД

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

Архив проекта: 35_sitewomen.zip

Продолжаем изучение ORM Django. Часто вызов агрегирующих функций применяется не ко всем записям, а к группам, сформированным по определенному полю. Например, в таблице Women можно сгруппировать записи по cat_id и получим две независимые группы записей. Затем, к каждой группе применить агрегацию и получить искомые значения.

В качестве примера, давайте подсчитаем число постов для каждой группы категорий. Для этого запишем такую команду:

Women.objects.values("cat_id").annotate(Count("id"))

Ее действие графически можно представить так:

То есть, здесь группировка автоматически выполняется по единственному полю, которое мы выбираем из таблицы. При необходимости можем изменить имя параметра id__count, скажем, на total, указав его явно в методе annotate():

Women.objects.values('cat_id').annotate(total=Count('id'))

Вот так делается группировка записей с помощью метода values() и вызов агрегирующих функций через метод annotate().

Мало того, далее, мы можем прописывать другие методы, после метода annotate(). Например вот так делается отбор рубрик, у которых число постов больше нуля:

lst = Category.objects.annotate(total=Count("posts")).filter(total__gt=0)

Из SQL-запроса видно, что в этом случае происходит группировка по всем полям таблицы, поэтому ее как бы и нет (хотя бы потому, что поле id уникально).

Отобразим результаты в консоли:

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

Получим:

['id', 'name', 'slug', 'total']
[1, 'Актрисы', 'aktrisy', 5]
[2, 'Певицы', 'pevicy', 5]

По аналогии мы можем отобрать все теги из таблицы tagpost, которым соответствует хотя бы одна статья:

lst = TagPost.objects.annotate(total=Count("tags")).filter(total__gt=0)

Надо сказать, что подобные запросы получаются не самые быстрые, поэтому перед их применением нужно оценить необходимость получения таких составных данных непосредственно из СУБД. Возможно, проще получить список данных, а затем, через цикл провести их дополнительную обработку.

Давайте воспользуемся этим запросом для вывода только значащих тегов. Для этого перейдем в файл women_tags.py и перепишем функцию show_all_tags() следующим образом:

@register.inclusion_tag('women/list_tags.html')
def show_all_tags():
    return {"tags": TagPost.objects.annotate(total=Count("tags")).filter(total__gt=0)}

Запускаем тестовый веб-сервер и у нас теперь отображается только два тега. Но присутствуют три рубрики, одна из которых («Спортсменки») пустая. Давайте для вывода рубрик сделаем то же самое:

@register.inclusion_tag('women/list_categories.html')
def show_categories(cat_selected_id=0):
    cats = Category.objects.annotate(total=Count("posts")).filter(total__gt=0)
    return {"cats": cats, "cat_selected": cat_selected_id}

Теперь видим только заполненные рубрики. Вот пример того, как можно использовать такие сложные запросы с небольшим набором данных в таблицах.

Вычисления на стороне СУБД

В заключение этого занятия отмечу, что фреймворк Django содержит набор функций, позволяющие выполнять вычисления, связанные с полями таблицы, на стороне СУБД. Полный их список можно посмотреть по ссылке:

https://docs.djangoproject.com/en/4.2/ref/models/database-functions/

Фактически, здесь приведены обертки над функциями, которые выполняются в СУБД. Этих функций достаточно много. Это и функции работы со строками, датой, математические функции и так далее. Использование этих функций является рекомендуемой практикой, т.к. СУБД оптимизировано для их выполнения. Конечно, все имеет свои разумные пределы и нужно лишь по необходимости прибегать к этому функционалу.

Давайте для примера рассмотрим использование функции Length для вычисления длины строки. Первым делом нам нужно ее импортировать:

from django.db.models.functions import Length

И, затем, аннотируем новое вычисляемое поле, например, для имен из таблицы husband:

lst = Husband.objects.annotate(len_name=Length('name'))

В результате, наряду со всеми стандартными полями, получим дополнительное поле len_name:

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

По аналогии используются все остальные подобные функции.

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

Видео по теме