Django ORM

Введение

ORM или Object Relational Mapping это достаточно мощный инструмент Django, который позволяет взаимодействовать с БД используя синтаксис Python, не опускаясь при этом до уровня SQL запросов, несмотря на это, знать синтаксис SQL тоже не помешает. Более того Django ORM позволяет писать одинаково оптимизированные запросы к БД поддерживаемой любой СУБД, будь то PostgreSQL, Oracle и прочие. Рассматривать возможности Django ORM будем на примере БД сайта, который мы писали в блоке 'Django Углубление' и перед тем начать знакомство с методами рассмотрим еще одну тему, которую как мне кажется логичнее разобрать именно в этом блоке. Я говорю об оптимизации сайта с использованием Django Debug Toolbar.

Оптимизация работы сайта. Django Debug Toolbar

Каждый раз, когда мы пользуемся сайтом для отрисовки данных мы обращаемся к БД, соответственно мы используем для этого SQL запросы, частота и сложность этих запросов влияют на скорость работы сайта, поэтому имеет смысл оптимизировать эти запросы. Для того чтобы отследить количество частоту и сложность этих запросов существует инструмент Django Debug Toolbar, проведем его установку следуя документации и посмотрим какие возможности предоставляет этот инструмент.

Необходимо добавить 'debug_toolbar' в INSTALLED_APPS, 'debug_toolbar.middleware. DebugToolbarMiddleware' в MIDDLEWARE и добавить в setting.py новую коллекцию INTERNAL_IPS с адресом 127.0.0.1.

pylibrary/urls.py
if settings.DEBUG:

    urlpatterns = [
        path('__debug__/', include('debug_toolbar.urls')),
    ] + urlpatterns
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Что касается urls.py, путь для Debug Toolbar следует прописать при условии включенного DEBUG режима, помимо этого следует прибавить коллекцию urlpatterns к этому пути, в противном случае при запуске сервера ни одной страницы найдено не будет.

Теперь можно перейти на сайт и посмотреть на изменения.

Справа у нас появилась панель с некоторыми подсказками, есть время за которое отрисовывается страница, используемы для этого шаблоны и то, что нас на данный момент интересует больше всего - вкладка SQL. В этой вкладке мы можем увидеть все используемые SQL запросы для данной страницы. На примере я перешел на страницу категории 'роман' и открыв SQL увидел, что на этой странице есть два дублирующихся запроса. На эти запросы тратится лишнее время и, конечно, этого хотелось бы избежать.

Каждый из запросов мы можем развернуть и увидеть, в каком месте программы формируются эти лишние запросы. В данном случае мы видим, что лишний запрос формируется для вывода title этой страницы. Перейдем во views.py и посмотрим, что мы можем с этим сделать.

book/views.py
class CategoryInfoView(DataMixin, ListView):
    template_name = 'book/books_for_category.html'

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super().get_context_data(**kwargs)
        cat = Category.objects.get(slug=self.kwargs['category_slug'])
        context['title'] = str(cat.name)
        return context
...

Для передачи категории в title давайте сразу прочитаем ее из БД и обратимся через эту переменную к параметру name.

Вернувшись на сайт мы видим, что запросов стало на один меньше и дублирующихся запросов у нас больше нет. Так с помощью Django Debug Toolbar можно проанализировать работу вашего сайта и заняться вопросом скорости работы вашего сайта.

select_related(). prefetch_related()

На данный момент единственный дублирующиеся запросы оказались связаны с выводом title категории, но хотелось бы разобрать еще один возможный пример для знакомства с другими возможностями решения дублирующихся запросов. На данный момент на главной странице мы выводим название, авторов и год, но не выводим категорию, давайте выведем еще и категорию для наглядности того, о чем я хочу рассказать.

book/main.html
{% extends 'book/base.html' %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
        {% for book in books %}
        <h2><a href="{{ book.get_absolute_url }}" class="link">{{ book.title }}</a></h2>
            {% for author in book.authors.all %}
                <h5>{{ author.name }}</h5>
            {% endfor %}
        <p>Категория - {{ book.category }}</p>
        <p>Год выпуска - {{ book.year }} год.</p>
        {% endfor %}
{% endblock %}

Выведем категорию через книгу и посмотрим что стало с запросами теперь. 15 запросов.

Мы видим целых 4 повторяющихся запроса, которые связаны именно с выводом категории. Это происходит из-за того, что категория выводится с помощью так называемого 'ленивого' запроса. То есть, каждый раз пробегаясь в цикле мы для каждой книги делаем запрос к базе для вывода ее категории, более производительным был бы вариант, когда мы сразу бы получали эти категории с queryset'ом. И такие запросы есть, они называются 'жадные'.

Убедиться, что проблема именно в этом можно развернув запрос.

'Жадные' запросы существуют двух типов:
select_related() - для обращения к данным связанным с моделью с помощью ForeignKey
prefetch_related- для обращения к данным связанным с моделью с помощью ManyToManyField
Идея заключается в том, что мы сразу забираем в queryset поля моделей связанных с основной моделью, которые нам могут пригодиться.

book/views.py
class HomeView(DataMixin, ListView):
    template_name = 'book/main.html'
    extra_context = {'title': 'Библиотека'}

    def get_queryset(self):
        return Book.objects.all().select_related('category')

Решить эту проблему можно следующим образом. Отрисовка категорий для всех книг происходит в файле main.html, с этим файлом связан View с названием HomeView, значит переопределять queryset нам нужно именно для этого View. Категории связаны с книгой с помощью ForeignKey, значит воспользуемся select_related(). В скобках этих методов записываются названия полей, по которым мы связаны с дочерними моделями. Так поле для связи модели Book с моделью Category называется category, и таким переопределением метода get_queryset мы говорим что нужно сразу забрать еще и эти данные.

И действительно, вернувшись теперь на туже страницу мы увидим 10 запросов, вместо 15. И пускай сейчас мы таким способом сократили время загрузки всего на 3мс, но когда речь идет о базах данных с десятками тысяч записей, подобного рода оптимизация может заметно ускорить работу сайта.

Python shell. connection. Objects. QuerySet

Для демонстрации возможностей Django ORM будем использовать встроенную оболочку shell. Войти в нее можно командой
python manage.py shell

Terminal
(venv) tsarkoilya@tsarkoilya-NBLK-WAX9X:~/kavo/KAVO/pylibrary$ python manage.py shell
Python 3.8.10 (default, Mar 15 2022, 12:22:08)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from book.models import *
>>> Author.objects.all()
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

Перейдем в оболочку и импортируем все модели нашего сайта с помощью оператора *.
Самый первый и простой способ выбрать записи из базы данных .all(), так выбрав все данные из модели Author мы получаем QuerySet содержащий все записи, которые при условии, что в классе мета не прописан параметр Ordering выведутся в порядке добавления их в БД. QuerySet это список объектов, а объекты это экземпляры класса представляющего модель.
Если проваливаться в сам QuerySet Django мы увидим, что это класс, в котором и написаны методы ORM, тот же all(), filter(), get() и так далее. objects, через который мы обращаемся к методам QuerySet'а, это менеджер, промежуточный класс, который так и называется Manager. Сам класс Manager наследуется от класса BaseManager, который содержит свои методы, например, get_queryset(), внутри которого мы выбираем записи QuerySet'а, по конкретным признакам.
Наличие промежуточного класса позволяет достаточно просто переопределять собственный manager со своими методами, этим мы займемся позже.

Весь ORM это SQL запросы, для которых используется синтаксис языка этого ORM. Убедиться, что это так и посмотреть на эти запросы можно тут же в shell. Для этого находясь в shell нужно импортировать connection командой
from django.db import connection

Terminal
>>> from django.db import connection
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 21', 'time': '0.011'}]

У connection есть метод queries, который выводит все SQL запросы выполненные в рамках этой оболочки. Таким образом перед нами один сформированный SQL запрос, где написано, что мы выбираем все столбцы из модели book_author. Формирование названия таблицы вида (имя приложения)_(имя модели) это стандартно определенное поведение, которое тоже можно переопределить. В этом запросе мы можем видеть время, за которое он отработал и параметр LIMIT, который определен в Django по умолчанию в константе MAX_GET_RESULT.

Сформировав начальное представление о том, что из себя представляет Django ORM давайте начнем знакомиться с его методами.

all(). get(). filter(). Сопутствующие методы

Для того, чтобы очистить список connection.queries надо выйти из shell, для этого используется команда
exit()
Импортировать нужные модели и connection нужно будет заново.

Terminal
>>> Author.objects.all()[:3]
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>]>
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 3', 'time': '0.001'}]

>>> Author.objects.all()[:3:2]
[<Author: Адитья Бхаргава>, <Author: Бен Страуба>]
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 3', 'time': '0.001'}, {'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 3', 'time': '0.001'}]

>>> Author.objects.all()[2:4]
<QuerySet [<Author: Бен Страуба>, <Author: Евгений Моргунов>]>
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 3', 'time': '0.001'}, {'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 3', 'time': '0.001'}, {'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" LIMIT 2 OFFSET 2', 'time': '0.001'}]

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

И из этого примера хорошо видно, что connection.queries хранит все запросы в рамках оболочки, и последний запрос добавляется в конце списка.

Terminal
>>> Author.objects.filter()
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.filter(pk__gte=2)
<QuerySet [<Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.filter(pk__lte=2)
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>]>

>>> Author.objects.filter(country__isnull=True)
<QuerySet [<Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Уильям Берроуз>]>

>>> Author.objects.filter(country__isnull=False)
<QuerySet [<Author: Адитья Бхаргава>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.filter(pk=2)
<QuerySet [<Author: Скотт Чакон>]>

Метод filer() без атрибутов вернет такой же QuerySet как и метод all().
Для фильтрации можно использовать огромное количество признаков, например, __gte и __lte означают больше равно и меньше равно соответственно, __isnull в значении True и False это проверка на Null. Но главное, на что нужно обратить внимание all() и filter() всегда возвращают QuerySet. Даже выбрав одну запись, мы все равно получим QuerySet.
Поскольку QuerySet, он поддерживает обход в цикле, чем мы активно пользуемся в шаблонах. Но зачем нам QuerySet, когда забираем одну запись.

Terminal
>>> Author.objects.get(pk=2)
<Author: Скотт Чакон>

>>> Author.objects.get(id=2)
<Author: Скотт Чакон>

>>> Author.objects.get(name__startswith='Ск')
<Author: Скотт Чакон>

>>> Author.objects.get(name__contains='тт')
<Author: Скотт Чакон>

Для этого у нас есть метод get(), который всегда возвращает одну запись, если попробуете взять несколько записей, то вы получите ошибку. Для выборки записей методом get() также можно использовать большое количество методов. Это может быть просто pk или id, при чем в рамках ORM это одно и тоже, просто сообществом имя pk для поля id общепринятое правило. Можно использовать метод __startswith, который говорит что запись должна начинаться с содержимого, переданного в эту переменную. Или можно использовать метод __contains, который означает, что содержимое может встретиться в любом месте записи. При чем экранирующие символы %, используемы для аналогичных выборок на чистом SQL уже учтены в этих методах.

Остальные методы, которые возвращают QuerySet

Методы filter() и all() не все методы, возвращающие QuerySet, но они самые часто используемые. Посмотрим на остальные.

Terminal
>>> Author.objects.exclude(pk__gt=3)
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>]>

>>> Author.objects.exclude(pk__lt=3)
<QuerySet [<Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.exclude(name__contains='тт')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

Метод exclude(). Возвращает все записи, которые не соответствуют условию. Так в первом примере вернем записи, pk которых не больше 3, а в следующем - pk которых не меньше 3.

Terminal
>>> Author.objects.order_by('name')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Скотт Чакон>, <Author: Уильям Берроуз>, <Author: Энн Шульгина>]>

>>> Author.objects.order_by('-name')
<QuerySet [<Author: Энн Шульгина>, <Author: Уильям Берроуз>, <Author: Скотт Чакон>, <Author: Евгений Моргунов>, <Author: Бен Страуба>, <Author: Александр Шульгин>, <Author: Александр Иванович Герцен>, <Author: Адитья Бхаргава>]>

>>> Author.objects.order_by('name', 'country')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Скотт Чакон>, <Author: Уильям Берроуз>, <Author: Энн Шульгина>]>

Метод order_by(). Вернет QuerySet отфильтрованных по заданным полям записей. Знак минус перед именем поля отсортирует содержимое в обратном порядке. Возможно сортировка не по одному полю, в таком случае сначала происходит по первому полю, а далее сортировка по следующему, в нашем примере не у всех авторов заданы страны, такие авторы оказались в конце списка.

Terminal
>>> Author.objects.reverse()
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.reverse()[:3]
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>]>

>>> Author.objects.reverse()[3:]
<QuerySet [<Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

Метод reverse(). Возвращает все записи отсортированные в обратном порядке, обратный метод метода all().

Terminal
>>> books = Book.objects.using('default')
>>> for book in books:
...           print(book.title)
...
Git для профессионального программиста
PostgreSQL. Основы языка SQL
test book
Грокаем алгоритмы
Джанки
Кто виноват?
Фенэтиламины, которые я знал и любил

>>> books = Book.objects.all().using('default')
>>> books
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: test book>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

Метод using(). Используется, когда у вас в проекте несколько баз данных, в качестве аргумента принимает alias базы данных из переменной DATABASES файла settings.py, обратите внимание alias это не name, alias это общее имя базы данных в переменной DATABASES.

Terminal
>>> Book.objects.distinct()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: test book>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> Book.objects.order_by('category__name').distinct('category__name')
<QuerySet [<Book: Джанки>, <Book: test book>, <Book: Кто виноват?>]>

>>> Book.objects.order_by('category__name').distinct('category__name')
<QuerySet [<Book: Джанки>, <Book: PostgreSQL. Основы языка SQL>, <Book: Кто виноват?>]>

Метод distinct(). Возвращает уникальные записи из базы данных. Если мы применим этот метод к модели book мы увидим все записи, а вот если отсортируем все книги по категориям, то увидим только по одной книге и каждой категории. При чем, если категория содержит несколько книг мы увидим последнюю добавленную к этой категории книгу, если ее удалить из БД, то увидим книгу, добавленную до нее.

При чем запись с выбором записей по конкретным полям доступна не для всех СУБД, в sqlite, например, такая запись будет недоступна.

Методы, которые возвращают подклассы QuerySet. values(). list_values(). none()

Если обычный QuerySet представляет собой список экземпляров класса, подклассы возвращают преобразованные к другим типам данных экземпляры. Таких подклассов не очень много, среди них: ValuesQuerySet, ValuesListQuerySet.

Terminal
>>> Category.objects.all().values()
<QuerySet [{'id': 1, 'name': 'Программирование', 'slug': 'programmirovanie'}, {'id': 2, 'name': 'Биография', 'slug': 'biografiya'}, {'id': 3, 'name': 'Роман', 'slug': 'roman'}]>

>>> Category.objects.all().values('id', 'name')
<QuerySet [{'id': 1, 'name': 'Программирование'}, {'id': 2, 'name': 'Биография'}, {'id': 3, 'name': 'Роман'}]>

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

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

Terminal
>>> cat = Category.objects.all().values('id', 'name')
>>> cat.get(pk=1)
{'id': 1, 'name': 'Программирование'}

>>> cat1 = cat.get(pk=1)
>>> cat1['id']
1

Поскольку объект ValuesQuerySet это объект словаря мы можем обращаться с ним как со словарем.

Terminal
>>> Category.objects.all().values_list()
<QuerySet [(1, 'Программирование', 'programmirovanie'), (2, 'Биография', 'biografiya'), (3, 'Роман', 'roman')]>

>>> Category.objects.all().values_list('id', 'name')
<QuerySet [(1, 'Программирование'), (2, 'Биография'), (3, 'Роман')]>

Метод list_values(). Работает точно также как и values(), но вместо словарей возвращает множества.

Terminal
>>> Book.objects.none()
<QuerySet []>

Метод none(). Возвращает QuerySet, который не содержит объектов. Такой тип QuerySet'a называется EmptyQuerySet.

Добавление и редактирование записей через shell. dates(). datetimes()

Для демонстрации работы методов dates() и datetimes() нам нужная модель, в которой есть поле типа дата. Создавать новые модели через shell нельзя, поэтому создадим ее стандартно в models.py.

book/models.py
...
class Test(models.Model):
    name = models.CharField(blank=True, max_length=200)
    create_at = models.DateTimeField(auto_now_add=True)

Создадим простейшую модель Test с двумя полями. Нам нужно поле типа дата, воспользуемся DateTimeField. Сделаем миграции, перезапустим shell и снова импортируем все модели.

Создавать модели через shell мы не можем, а вот добавлять, редактировать и удалять записи мы можем.

Terminal
>>> first = Test.objects.create(name='test1')
>>> first
<Test: Test object (1)>

>>> first.create_at
datetime.datetime(2022, 5, 29, 17, 9, 1, 871423, tzinfo=datetime.timezone.utc)
>>> first.save()

>>> Test.objects.all()
<QuerySet [<Test: Test object (1)>]>

>>> first.name = 'test2'
>>> first.save()

>>> first = Test.objects.get(pk=1)
>>> first.name
'test2'

>>> Test.objects.get(pk=1).delete()
(1, {'book.Test': 1})
>>> Test.objects.all()
<QuerySet []>

На данный момент модель не содержит записей для добавления новой нужно написать метод .crete(), внутри которого задать значение для полей модели. Работает все также как вне shell, заполнять необходимо те поля, которые обязательны для заполнения. И всю эту запись необходимо присвоить переменной, к которой после применить метод .save(). Новая запись создана. Из-за auto_now_add=True дата создания будет добавлять автоматически, а из-за отсутствия метода __str__ мы видим объект с именем Test object (1).
Для редактирования записи стоит забрать ее в переменную и изменить интересующее нас поле, не забудьте применить метод .save() для сохранения изменений.
Для удаления записей используется метод .delete(). Выбрать записи для удаления можно разными способами, например, через .get(), как в примере, или несколько записей методом .filter().

Terminal
>>> Test.objects.dates('create_at', 'month')
<QuerySet [datetime.date(2022, 5, 1)]>

>>> Test.objects.all().dates('create_at', 'month')
<QuerySet [datetime.date(2022, 5, 1)]>

>>> Test.objects.all().dates('create_at', 'day')
<QuerySet [datetime.date(2022, 5, 29), datetime.date(2022, 5, 30)]>

>>> Test.objects.all().dates('create_at', 'year')
<QuerySet [datetime.date(2022, 1, 1)]>

Метод dates(). Возвращает DateQuerySet. Принимает два обязательных аргумента, название поля и 'year', 'month' или 'day' в зависимости от потребностей. dates() возвращает количество уникальных значений для года, месяца и дня для всех записей этого поля. Так, например, я добавил в модель еще одну запись на следующий день и в итоге мы видим что все записи добавлены в один год, в один месяц, но в разные дни.

Terminal
>>> Test.objects.all().datetimes('create_at', 'hour')
<QuerySet [datetime.datetime(2022, 5, 29, 18, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC')), datetime.datetime(2022, 5, 30, 10, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC'))]>

>>> Test.objects.all().datetimes('create_at', 'minute')
<QuerySet [datetime.datetime(2022, 5, 29, 18, 14, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC')), datetime.datetime(2022, 5, 30, 10, 8, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC'))]>

>>> Test.objects.all().datetimes('create_at', 'second')
<QuerySet [datetime.datetime(2022, 5, 29, 18, 14, 2, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC')), datetime.datetime(2022, 5, 30, 10, 8, 50, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC'))]>

Метод datetimes(). Возвращает DateTimeQuerySet. Работает также как dates(), но помимо года, месяца и дня есть возможность отсортировать записи по дням, минутам и секундам.

Отсюда становится понятно, что dates() уместен для DateField, а datetimes() для DateTimeField.

select_related(). prefetch_related(). defer(). only()

Раз уж мы тут разбираем методы, которые возвращаю QuerySet, то давайте еще раз поговорим о методах select_related() и prefetch_related(), мы посмотрели как 'жадные' запросы применяются на практике и как их использование помогает уменьшить количество SQL запросов для загрузки страницы.

select_related() можно применять не только к связанным с помощью ForeignKey, но и к моделям связанных с помощью OneToOneField. Каждый раз, когда мы хотим обратиться к базе данных мы отправляем к ней запрос, 'жадные' запросы помогают нам изменить это поведение.

Terminal
>>> Book.objects.get(pk=1)
<Book: Грокаем алгоритмы>

>>> Book.objects.select_related().get(pk=1)
<Book: Грокаем алгоритмы>

>>> ex_1 = Book.objects.get(pk=1)
>>> ex_2 = Book.objects.select_related().get(pk=1)
>>> ex_1 = ex_1.category
>>> ex_2 = ex_2.category

>>> ex_1
<Category: Программирование>
>>> ex_2
<Category: Программирование>

Перед нами два одинаковых по функционалу запроса. Важно понимать один момент, когда мы записали строку, например, Book.objects.get(pk=1), то мы не отправили запрос к БД мы его только сформировали, а вот когда мы присвоили отработку этой строки переменной, то запрос уже отправляется. И каждый раз, когда мы обращаемся к переменной, в которую записана работа менеджера, мы отправляем повторный запрос к БД. Таким образом, в случае Book.objects.get(pk=1) и переменной ex_1 мы отправили два SQL запроса, первый запрос случился, когда мы написали работу менеджера в ex_1, а второй, когда мы взяли через точку категорию из связанной модели. В случае Book.objects.select_related().get(pk=1) мы сделали только один запрос, в тот момент, когда передавали работу менеджера в переменную ex_2, а когда мы брали категорию книги, запроса к базе данных не произошло.

Terminal
>>> ex = Book.objects.select_related('category').get(pk=1)
>>> category = ex.category
>>> category
<Category: Программирование>

Мы можем явно не указывать названия связанных полей, тогда будут выбраны все поля, но рекомендуется все же явно указывать названия связанны полей.

Terminal
>>> books = Book.objects.prefetch_related('authors')
>>> for book in books:
...     print(book.authors.all())
...
<QuerySet [<Author: Скотт Чакон>, <Author: Бен Страуба>]>
<QuerySet [<Author: Евгений Моргунов>]>
<QuerySet [<Author: Адитья Бхаргава>]>
<QuerySet [<Author: Уильям Берроуз>]>
<QuerySet [<Author: Александр Иванович Герцен>]>
<QuerySet [<Author: Александр Шульгин>, <Author: Энн Шульгина>]>

prefetch_related() позволяет делать 'жадные' запросы для полей типа ManyToManyField. Но цель у prefetch_related() такая же, как у select_related() - избежать количества нарастающих запросов при работе со связанными моделями.

Вспомнив о работе select_related() и prefetch_related() можно перейти еще к двум похожим по назначению методам.

Terminal
>>> ex = Book.objects.defer('year').get(pk=1)
>>> ex.title
'Грокаем алгоритмы'

>>> ex.year
2019

>>> ex = Book.objects.prefetch_related('authors').defer('authors__country').get(pk=1)
>>> ex
<Book: Грокаем алгоритмы>

>>> ex.authors.all()
<QuerySet [<Author: Адитья Бхаргава>]>

>>> ex.authors.get(pk=1)
<Author: Адитья Бхаргава>

>>> ex = ex.authors.get(pk=1)
>>> ex.name
'Адитья Бхаргава'

>>> ex.country
'Индия'

Метод defer(). С помощью этого метода можно сказать какие поля забирать не стоит, например, возьмем первую запись из модели book, но запрос не заберет информацию из поля year. И когда мы обращаемся к .title мы не выполняем новый запрос, потому что информацию из title мы забрали, а для обращения к .description исполнится новый запрос. defer() можно сочетать с select_related() и prefetch_related() для 'жадной' выборки конкретных полей из связанных полей. Во втором примере 'жадно' заберем информацию об авторах первой книги, но при этом не станем забирать страну автора. В случае .name новый запрос не выполняется, а в случае .country - выполняется.

Terminal
>>> ex = Book.objects.prefetch_related('authors').only('authors__country').get(pk=1)
>>> ex = ex.authors.get(pk=1)

>>> ex.country
'Индия'

>>> ex.name
'Адитья Бхаргава'

Метод only(). Метод обратный методу defer(), извлекает содержание всех полей, которые переданы в этот метод. В данном случае для .country новый запрос не выполняется, а для .name - выполняется.

get(). create(). get_or_create(). update_or_create(). bulk_create()

Мы разобрали еще не все методы, которые возвращают QuerySet, оставшиеся более сложные методы мы разберем позже, а перейдем к знакомству с методами, которые не возвращают QuerySet.

С методами get() и create() мы уже знакомы, первый забирает одну запись по переданному признаку, второй создает одну новую запись в модель.

Terminal
>>> Book.objects.get_or_create(title='Грокаем алгоритмы')
(<Book: Грокаем алгоритмы>, False)
>>> Book.objects.get_or_create(title='Тест')
...
django.db.utils.IntegrityError...

>>> Test.objects.get_or_create(name='Тест')
(<Test: Test object (4)>, True)

>>> Book.objects.get_or_create(title='Тест', defaults={'year': 2019})
(<Book: Тест>, True)
>>> Book.objects.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Тест>, <Book: Фенэтиламины, которые я знал и любил>]>
>>> test = Book.objects.get(title='Тест')
>>> test.description
''
>>> test.title
'Тест'
>>> test.year
2019

Метод get_or_create(). Метод проверит наличие записей в БД по заданным значениям полей и в случае отсутствия записи создаст новую с переданными значениями. get_or_create() возвращает кортеж состоящий из экземпляра модели и True в случае, если эта запись была создана, а False в случае, если такая запись в БД есть.
Если мы попытаемся создать новую запись в модели Book с одни заполненным полем, мы получим ожидаемую ошибку, поскольку в нашей модели есть поля обязательные к заполнению, но нам ничего не мешает передать в get_or_create() несколько полей.
Также мы можем передать в этот метод необязательный словарь default. В этот словарь передаются поля со значением по-умолчанию. Если вдруг ваша модель использует defaults для названия поля, то вы можете воспользоваться фильтром default__exact=('значение для поля default по-умолчанию').
Если вдруг get_or_create() найдет несколько записей по заданным параметра, то будет возвращено исключение MultipleObjectsReturned.

Terminal
>>> Book.objects.update_or_create(title='Тест', defaults={'year': 2019})
(<Book: Тест>, False)

>>> Book.objects.update_or_create(title='Тест2', defaults={'year': 2019})
(<Book: Тест2>, True)

>>> Book.objects.update_or_create(title='Тест2', description='Тестовое описание', defaults={'year': 2019})
(<Book: Тест2>, True)

>>> Book.objects.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Тест>, <Book: Тест2>, <Book: Тест2>, <Book: Фенэтиламины, которые я знал и любил>]>

Метод update_or_create(). Работает также как и get_or_create(), но в случае наличия записи обновит значения в ней, в противном - создаст новую.

Terminal
>>> Test.objects.all()
<QuerySet [<Test: Test object (2)>, <Test: Test object (3)>, <Test: Test object (4)>]>

>>> Test.objects.bulk_create([Test(name='test5'), Test(name='test6')])
[<Test: Test object (5)>, <Test: Test object (6)>]

>>> Test.objects.all()
<QuerySet [<Test: Test object (2)>, <Test: Test object (3)>, <Test: Test object (4)>, <Test: Test object (5)>, <Test: Test object (6)>]>

Метод bulk_create(). Позволяет добавлять несколько записей за один раз.

first(). last(). earliest(). latest(). count(). in_bulk(). iterator()
Terminal
>>> Book.objects.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Тест>, <Book: Тест2>, <Book: Тест2>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> Book.objects.first()
<Book: Git для профессионального программиста>

>>> Book.objects.last()
<Book: Фенэтиламины, которые я знал и любил>

Метод first(). Возвращает первую запись модели.

Метод last(). Возвращает последнюю запись модели.

Terminal
>>> Author.objects.all()
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Евгений Моргунов>, <Author: Александр Иванович Герцен>, <Author: Уильям Берроуз>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Author.objects.latest('name')
<Author: Энн Шульгина>
>>> Author.objects.latest('country')
<Author: Скотт Чакон>
>>> Author.objects.latest('name', 'country')
<Author: Энн Шульгина>
>>> Author.objects.latest('country', 'name')
<Author: Уильям Берроуз>

>>> Author.objects.earliest('name')
<Author: Адитья Бхаргава>
>>> Author.objects.earliest('country')
<Author: Адитья Бхаргава>

Метод latest(). Вернет последнюю добавленную запись по переданным в этот метод названиям полей.

Метод earliest(). Вернет первую добавленную запись по переданным в этот метод названиям полей.

Terminal
>>> Author.objects.iterator()
<generator object QuerySet._iterator at 0x7f3ed16b1b30>
>>> iter = Author.objects.iterator()
>>> for i in iter:
...     print(i)
...
Адитья Бхаргава
Скотт Чакон
Бен Страуба
Евгений Моргунов
Александр Иванович Герцен
Уильям Берроуз
Александр Шульгин
Энн Шульгина

Метод iterator(). Возвращает объект QuerySet._iterator. iterator() читает результат сразу из базы данных, игнорирую кэширование на уровне QuerySet. QuerySet после обращения к БД и выборке необходимых данных кэширует их, чтобы обращаться к кэшу, а не снова отправлять запрос к БД. Поэтому применение метода iterator() для уменьшения нагрузки на производительность уместно только совместно с первым формированием QuerySet и только для тех QuerySet, который будут использованы один раз.

Terminal
>>> Author.objects.in_bulk()
{1: <Author: Адитья Бхаргава>, 2: <Author: Скотт Чакон>, 3: <Author: Бен Страуба>, 4: <Author: Евгений Моргунов>, 5: <Author: Александр Иванович Герцен>, 6: <Author: Уильям Берроуз>, 7: <Author: Александр Шульгин>, 8: <Author: Энн Шульгина>}

>>> Author.objects.in_bulk([1, 4])
{1: <Author: Адитья Бхаргава>, 4: <Author: Евгений Моргунов>}

>>> Author.objects.in_bulk([5])
{5: <Author: Александр Иванович Герцен>}

Метод in_bulk(). Преобразует QuerySet к словарю, ключ - арифметическая прогрессия, значение - запись модели. Можно передать значения ключей для вывода конкретных пар.

Terminal
>>> Author.objects.all().count()
8

>>> Author.objects.filter(name__startswith='А').count()
3

Метод count(). Возвращает количество записей по заданному признаку.

exists(). update(). delete()
Terminal
>>> Book.objects.filter(year=2019)
<QuerySet [<Book: Грокаем алгоритмы>, <Book: Тест>, <Book: Тест2>, <Book: Тест2>]>

>>> Book.objects.filter(year=2019).update(year=2022)
4
>>> Book.objects.filter(year=2018).update(year=2022)
2
>>> Book.objects.filter(year=2000).update(year=2022)
0

>>> Book.objects.filter(year=2022)
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Тест>, <Book: Тест2>, <Book: Тест2>]>

>>> Author.objects.all().update(country='Россия')
8

Метод update(). Метод для обновления данных указанных полей. Возвращает количество измененных записей, если ни одна запись не обновлена возвращает 0.

Terminal
>>> Book.objects.filter(title__startswith='Тес')
<QuerySet [<Book: Тест>, <Book: Тест2>, <Book: Тест2>]>

>>> test = Book.objects.filter(title__startswith='Тес')
>>> test.delete()
(3, {'book.Book': 3})

>>> Book.objects.filter(title__startswith='Тес')
<QuerySet []>

Метод delete(). Удаляет любую запись или несколько записей отобранных по любому признаку.

Terminal
>>> Book.objects.filter(title__startswith='Тес').exists()
False

>>> Book.objects.filter(title__startswith='Гро').exists()
True

Метод exists(). Возвращает True, если QuerySet содержит результат, False - если нет.

Функции агрегации. annotate(). aggregate()

Следующие два метода, которые мы рассмотрим напрямую связаны с функциями агрегации. Функции агрегации находятся в django.db.models. Сразу рассмотрим на примерах.

Terminal
>>> from django.db.models import *

>>> Book.objects.annotate(Max('year'))
<QuerySet [<Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Git для профессионального программиста>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>, <Book: PostgreSQL. Основы языка SQL>]>
>>> max_year = Book.objects.annotate(max=Max('year'))
>>> max_year[0].max
2022
>>> max_year[2].max
2022

>>> Book.objects.all().aggregate(Max('year'))
{'year__max': 2022}
>>> Book.objects.all().aggregate(Min('year'))
{'year__min': 1846}

Думаю на этом примере хорошо видно разницу между annotate() и aggregate(). Оба этих метода в качестве аргументов принимают функции агрегации. А функции агрегации в свою очередь принимают названия полей, для которых следует их применить. Метод aggregate() возвращает словарь агрегированных значений в формате название поля_функция агрегации.
Метод annotate() добавляет результат работы агрегирующей функции к каждому объекту, который возвращает QuerySet, для которого был применен метод annotate(). В случае annotate() удобнее задать имя для результата исполнения агрегирующей функции и через него обращаться к значению. Но если имя не задать, то обращаться к результату можно также как в случае aggregate() - название поля_функция агрегации.

Первые две функции агрегации, которые мы уже увидели - Max() и Min(). Возвращают они максимально и минимальное значение указанного поля соответственно.

Terminal
>>> max_year = Book.objects.annotate(max=Max('year'), min=Min('year'))

>>> max_year[3].min
1846
>>> max_year[5].min
2022
>>> max_year[3].max
1846

Конечно функции агрегации можно комбинировать.

Посмотрим на остальные функции агрегации.

Terminal
>>> Book.objects.all().aggregate(Avg('year'))
{'year__avg': 1978.0}

>>> Book.objects.all().aggregate(Count('year'))
{'year__count': 6}

>>> Book.objects.all().aggregate(Sum('year'))
{'year__sum': 11868}

>>> Book.objects.all().aggregate(StdDev('year'))
{'year__stddev': 63.877486905273074}

>>> Book.objects.all().aggregate(Variance('year'))
{'year__variance': 4080.3333333333335}

Avg() - среднее арифметическое.
Count() - количество объектов по указанному параметру.
Sum() - сумма всех значений.
StdDev() - Стандартное отклонение (Standard Deviation). На формуле сейчас останавливаться не станем, ее можно легко найти самостоятельно.
Variance() - дисперсия выбранных значений.
Как видно все функции агрегации возвращаю либо int, либо float и работаю с числовыми полями, кроме функции Count(), она применима к любым типам полей.

Terminal
>>> summ = Author.objects.aggregate(Count('name'), Count('country'))
>>> summ
{'name__count': 8, 'country__count': 8}

>>> summ_disc = Author.objects.aggregate(Count('name'), Count('country', distinct=True))
>>> summ_disc
{'name__count': 8, 'country__count': 1}

>>> Author.objects.aggregate(Count('name', output_field=FloatField()))
{'name__count': 8.0}

Функции агрегации имеют дополнительные параметры. distinct в значении True вернет только уникальные значения из выбранных, а с помощью output_field можно изменить тип возвращаемых данных.

extra(). raw(). select_for_update()

Если ваше условие достаточно сложное и стандартных возможностей ORM вам недостаточно вы можете воспользоваться методом extra(). Внутри extra() можно писать свой select, where, order_by и список params для передачи дополнительных WHERE параметров.

Terminal
>>> Book.objects.extra(select={'author': 'SELECT COUNT(*) FROM book_book_authors WHERE book_book.id = book_book_authors.book_id'})
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

Как видно метод extra() поддерживает синтаксис уровня ORM, пригодится это может в достаточно специфичных случаях.

Но если вам потребуется написать запрос не используя синтаксис ORM, а используя синтаксис чистого SQL, то для таких целей есть метод raw(). Внутри метода raw() пишется SQL запрос, в связи с этим не нужно забывать выбирать поле id, если вы решили выбрать конкретный набор полей, а не все используя оператор *.

Перед тем как запрос попадает на обработку во view Django открывает транзакцию, это включаемый параметр, за него отвечает константа ATOMIC_REQUESTS (по умолчанию False, а в случае True транзакции будут включены), задается этот параметр в переменной DATABASES для каждой БД отдельно. В случае правильной отработки запроса делается commit в БД, а в случае исключения rollback. Работой транзакций можно управлять вручную, один из способов - использование метода select_for_update(), который говорит что следует заблокировать (запрет на изменение) все выбранные записи до завершения транзакции. Завершается транзакция методом update().

Операторы фильтрации

С некоторыми операторами фильтрации мы уже познакомились ранее, например, __gt и __lt. Операторы фильтрации создают оператор WHERE, если опускаться до уровня SQL запросов.

Terminal
>>> Author.objects.get(name='Бен Страуба')
<Author: Бен Страуба>
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" = \'Бен Страуба\' LIMIT 21', 'time': '0.011'}]

>>> Author.objects.get(name__exact='Бен Страуба')
<Author: Бен Страуба>
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" = \'Бен Страуба\' LIMIT 21', 'time': '0.011'},
{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" = \'Бен Страуба\' LIMIT 21', 'time': '0.001'}]

>>> Author.objects.get(name__iexact='бен Страуба')
<Author: Бен Страуба>
>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" = \'Бен Страуба\' LIMIT 21', 'time': '0.011'},
{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" = \'Бен Страуба\' LIMIT 21', 'time': '0.001'},
{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE UPPER("book_author"."name"::text) = UPPER(\'бен Страуба\') LIMIT 21', 'time': '0.012'}]

Оператор WHERE присутствует в результатах запроса методов filter(), exclude() и get(), например, когда мы забираем одного автора по точному совпадению имени мы применяем запрос WHERE name = Бен Страуба. Точно такой же запрос мы увидим если применим оператор фильтрации __exact, которые применяется по умолчанию в любом запросе перечисленных выше методов и как раз за счет __exact в каждом из таких запросов мы видим оператор WHERE. Оператор фильтрации __exact означает точное совпадение, в паре с __exact существует оператор фильтрации __iexact, который означает регистронезависимое точное совпадение. В самом последнем запросе мы видим как это реализовано.

Terminal
>>> Book.objects.filter(pk__in=[1,4])   
<QuerySet [<Book: Грокаем алгоритмы>, <Book: Кто виноват?>]>

>>> Book.objects.filter(year__in=[1000, 3000])
<QuerySet []>

>>> connection.queries
[{'sql': 'SELECT "book_book"."id", "book_book"."title", "book_book"."slug", "book_book"."description", "book_book"."year", "book_book"."category_id", "book_book"."user_id" FROM "book_book" WHERE "book_book"."id" IN (1, 4) ORDER BY "book_book"."title" ASC LIMIT 21', 'time': '0.001'},
{'sql': 'SELECT "book_book"."id", "book_book"."title", "book_book"."slug", "book_book"."description", "book_book"."year", "book_book"."category_id", "book_book"."user_id" FROM "book_book" WHERE "book_book"."year" IN (1000, 3000) ORDER BY "book_book"."title" ASC LIMIT 21', 'time': '0.001'}]

Оператор фильтрации __in аналог оператору IN в SQL. Совершает проверку на вхождение в список значений, при чем если мы передаем значения мы проверяем не проверку в диапазон этих значений, а соответствие любому их переданных значений.

Terminal
>>> Author.objects.filter(name__icontains='а')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Скотт Чакон>, <Author: Бен Страуба>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>, <Author: Энн Шульгина>]>

>>> Book.objects.filter(authors__in=a_authors)
<QuerySet [<Book: Git для профессионального программиста>, <Book: Git для профессионального программиста>, <Book: Грокаем алгоритмы>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name" IN (SELECT U0."id" FROM "book_author" U0 WHERE UPPER(U0."name"::text) LIKE UPPER(\'%а%\')) LIMIT 21', 'time': '0.011'},
{'sql': 'SELECT "book_book"."id", "book_book"."title", "book_book"."slug", "book_book"."description", "book_book"."year", "book_book"."category_id", "book_book"."user_id" FROM "book_book" INNER JOIN "book_book_authors" ON ("book_book"."id" = "book_book_authors"."book_id") WHERE "book_book_authors"."author_id" IN (SELECT U0."id" FROM "book_author" U0 WHERE UPPER(U0."name"::text) LIKE UPPER(\'%а%\')) ORDER BY "book_book"."title" ASC LIMIT 21', 'time': '0.015'}]

Списком для оператора __in может являться другой QuerySet, например, выберем из модели Author всех авторов, в имени которых есть регистронезависимая буква 'а' и на основе этой выборки заберем все книги из модели Book написанных этими авторами. LIKE UPPER(\'%а%\') означает, что буква 'а' может быть в любом месте строки и в любом регистре. INNER JOIN формирует новую таблицу на основе двух или более таблиц. Так в данном примере мы сформировали новую таблицу на основе таблиц book и author, оператор ON при этом определяет параметры для формирования новой таблицы. В примере мы говорим, что id книги из модели book должно соответствовать id книги из модели author, при условии, что взяты будут только книги, авторы которых содержат в своем имени букву 'а' в любом регистре.

Terminal
>>> Book.objects.filter(year__range=(1000, 3000))
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> connection.queries
[{'sql': 'SELECT "book_book"."id", "book_book"."title", "book_book"."slug", "book_book"."description", "book_book"."year", "book_book"."category_id", "book_book"."user_id" FROM "book_book" WHERE "book_book"."year" BETWEEN 1000 AND 3000 ORDER BY "book_book"."title" ASC LIMIT 21', 'time': '0.001'}]

Если вернуться к вопросу выборки данных для переданного диапазона значений, то для этих целей используется оператор __range. __range делает проверку на вхождение в диапазон, в основе лежит оператор BETWEEN в SQL.

Terminal
>>> Test.objects.filter(create_at__date=datetime.date(2022, 5, 30))
<QuerySet [<Test: Test object (3)>, <Test: Test object (4)>, <Test: Test object (5)>, <Test: Test object (6)>]>

>>> Test.objects.filter(create_at__date__gt=datetime.date(2021, 4, 15))
<QuerySet [<Test: Test object (2)>, <Test: Test object (3)>, <Test: Test object (4)>, <Test: Test object (5)>, <Test: Test object (6)>]>

>>> connection.queries
[{'sql': 'SELECT "book_test"."id", "book_test"."name", "book_test"."create_at" FROM "book_test" WHERE ("book_test"."create_at" AT TIME ZONE \'UTC\')::date = \'2022-05-30\'::date LIMIT 21', 'time': '0.001'},
{'sql': 'SELECT "book_test"."id", "book_test"."name", "book_test"."create_at" FROM "book_test" WHERE ("book_test"."create_at" AT TIME ZONE \'UTC\')::date > \'2021-04-15\'::date LIMIT 21', 'time': '0.001'}]

Оператор __date проверяет поля типа дата и время на соответствие переданной дате.

Terminal
>>> Test.objects.filter(create_at__year=2022)
<QuerySet [<Test: Test object (2)>, <Test: Test object (3)>, <Test: Test object (4)>, <Test: Test object (5)>, <Test: Test object (6)>]>

>>> Test.objects.filter(create_at__month__lt=10)
<QuerySet [<Test: Test object (2)>, <Test: Test object (3)>, <Test: Test object (4)>, <Test: Test object (5)>, <Test: Test object (6)>]>

>>> connection.queries
[{'sql': 'SELECT "book_test"."id", "book_test"."name", "book_test"."create_at" FROM "book_test" WHERE "book_test"."create_at" BETWEEN \'2022-01-01T00:00:00+00:00\'::timestamptz AND \'2022-12-31T23:59:59.999999+00:00\'::timestamptz LIMIT 21', 'time': '0.001'}, {'sql': 'SELECT "book_test"."id", "book_test"."name", "book_test"."create_at" FROM "book_test" WHERE EXTRACT(\'month\' FROM "book_test"."create_at" AT TIME ZONE \'UTC\') < 10 LIMIT 21', 'time': '0.003'}]

Можно совершать проверку не на полноформатную дату, а совершать выборку по конкретному параметру даты. __year для года, __month для месяца и так далее вплоть до секунд.

Terminal
>>> Author.objects.filter(name__regex=r'^(А|Б)')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Бен Страуба>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>]>
>>> Author.objects.filter(name__iregex=r'^(а|б)')
<QuerySet [<Author: Адитья Бхаргава>, <Author: Бен Страуба>, <Author: Александр Иванович Герцен>, <Author: Александр Шульгин>]>

>>> connection.queries
[{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name"::text ~ \'^(А|Б)\' LIMIT 21', 'time': '0.011'},
{'sql': 'SELECT "book_author"."id", "book_author"."name", "book_author"."country" FROM "book_author" WHERE "book_author"."name"::text ~* \'^(а|б)\' LIMIT 21', 'time': '0.010'}]

Для фильтрации с использованием регулярных выражений используются фильтры __regex и __iregex, которые означают регистрозависимое и регистронезависимое соответствие переданным регулярным выражениям. Синтаксис регулярных выражений в Django ORM соответствует модулю re.

С остальными операторами фильтрации мы знакомились ранее, снова о них говорить не будем. Просто соберем все фильтры в итоговую таблицу.

Условия фильтрации
lookup Результат работы
__exact Точно совпадение
__iexact Регистронезависимое точное совпадение
__lt Меньше чем
__gt Меньше чем
__lte Меньше или равно чем
__gte Больше или равно чем
__contains Проверка на вхождение
__icontains Регистронезависимая проверка на вхождение
__in Проверка на вхождение список значений
__range Проверка на вхождение в диапазон значений
__isnull Принимает True либо False для проверки поля на NULL
__startswith Проверяет начинается ли поле с переданного значения
__istartswith Проверяет начинается ли поле с регистронезависимого переданного значения
__endswith Проверяет заканчивается ли поле на переданное значение
__iendswith Проверяет заканчивается ли поле на регистронезависимое переданное значение
__regex Проверка регулярных выражений
__iregex Регистронезависимая проверка регулярных выражений
__date Проверка на соответствие дате
__year Проверка года
__iso_year Проверка года в формате ISO 8601
__month Проверка месяца
__day Проверка дня
__week_day Проверка дня недели
__hour Проверка часа
__minute Проверка минуты
__second Проверка секунды
__search Полнотекстовый поиск, по умолчанию доступен для MySQL, чтобы использовать в PostgreSQL необходимо добавить django.contrib.postgres в INSTALLED_APPS

Мы уже использовали _set при написании сайта pylibrary. _set позволяет обращаться к связанным моделям.

Terminal
>>> category=Category.objects.get(pk=1)
>>> category
<Category: Программирование>
>>> category.book_set
<django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager object at 0x7f85c9f55880>
>>> category.book_set.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>]>

>>> author=Author.objects.get(name='Бен Страуба')
>>> author
<Author: Бен Страуба>
>>> author.book_set.all()
<QuerySet [<Book: Git для профессионального программиста>]>

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

Помимо выборки данных из связанных моделей есть необходимости в добавлении запись в связанные модели.

Terminal
>>> example_book=Book.objects.get(pk=2)
>>> example_book
<Book: Git для профессионального программиста>

>>> category=Category.objects.get(pk=2)
>>> category
<Category: Биография>

>>> category.book_set.all()
<QuerySet [<Book: Джанки>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> example_book.category=category
>>> example_book.save()
>>> category.book_set.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: Джанки>, <Book: Фенэтиламины, которые я знал и любил>]>

Вот так можно обновлять данные в моделях связанных полем ForeignKey. Так в примере мы выбрали конкретную книгу, которая принадлежала другой категории и конкретную категорию и в итоге заменили поле с категорией у этой книги.

Terminal
>>> new_category = Category.objects.create(name='Тестовая категория')
>>> new_category
<Category: Тестовая категория>

>>> example_book = Book.objects.get(pk=3)
>>> example_book
<Book: PostgreSQL.Основы языка SQL>

>>> new_category.book_set.all()
<QuerySet[]>

>>> example_book.category = new_category
>>> example_book.save()
>>> new_category.book_set.all()
<QuerySet[ < Book: PostgreSQL.Основы языка SQL >]>

Либо мы можем создать новую запись в модели категорий и сразу записать в эту новую категорию какую-нибудь книгу.

Обновление ManyToManyField выглядит немного иначе. Для ManyToManyField используется метод add().

Terminal
>>> example_book=Book.objects.get(pk=4)
>>> example_book
<Book: Кто виноват?>

>>> author=Author.objects.last()
>>> author
<Author: Адитья Бхаргава>

>>> author.book_set.all()
<QuerySet [<Book: Грокаем алгоритмы>]>

>>> example_book.authors.add(author)
>>> example_book.save()

>>> author.book_set.all()
<QuerySet [<Book: Грокаем алгоритмы>, <Book: Кто виноват?>]>

(во время работы над примерами я случайно заменил все записи в модели Author, потом вернул все на место, поэтому теперь они находятся не на своих местах, поэтому если вы следили за происходящем внимательно не обращайте внимания, что last() вернул 'Адитья Бхаргава', вместо 'Энн Шульгина'). Выглядит этот таким образом, принцип точно такой же как у ForeignKey полей. Так мы добавили к книге еще одного автора.

Класс Q. Класс F

Мы можем выбирать записи по нескольким условиям.

Terminal
>>> Book.objects.filter(pk__lte=3, category_id=2)
<QuerySet [<Book: Git для профессионального программиста>]>

>>> Book.objects.filter(pk__lte=3, category_id=3)
<QuerySet []>

Например, вот так мы выбираем книги id, которых меньше или равен 3 и которые принадлежат к категории с id 2. Запятая между условиями это логическое И, то есть обязательное совпадение по первому и второму критерию выборки. Но что если мы хотим выбрать записи, которые соответствуют хотя бы одному из этих критериев. Для этого в Django ORM есть класс Q.

Terminal
>>> from django.db.models import Q

>>> Book.objects.filter(Q(pk__lte=3) | Q(category_id=2))
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> Book.objects.filter(Q(pk__lte=3) | Q(category_id=3))
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Кто виноват?>]>

Класс Q находится в django.db.models и позволяет использовать логическое И, логическое ИЛИ и логическое НЕ. Так в примере мы используем ту же выборку, что и в примере выше, но каждый критерий фильтрации помещается в класс Q и между ними ставится символ вертикальной черты (пайп). Такая запись означает логическое ИЛИ, мы выбираем все записи, которые соответствуют хотя бы одному из этих критериев.

Terminal
>>> Book.objects.filter(pk__lte=3, category_id=3)
<QuerySet []>

>>> Book.objects.filter(Q(pk__lte=3) & Q(category_id=3))
<QuerySet []>

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

Terminal
>>> Book.objects.filter(~Q(pk__lte=3) & Q(category_id=3))
<QuerySet [<Book: Кто виноват?>]>

Для логического НЕ используется знак тильды.

Если в одном запросе используется несколько операторов класса Q, то срабатывать они будут в зависимости от их приоритетности. Приоритет у них следующий: 1 - логическое НЕ, 2 -логическое И, 3 - логическое ИЛИ.

В лукапах мы используем сторонние значения, будь-то цифры, буквы, другие символы, но что если мы в качестве аргумента лукапа хоти использовать другое поле таблицы. Для этой цели существует класс F все из того же django.db.models.

Terminal
>>> Book.objects.filter(category=F('id'))
<QuerySet [<Book: Git для профессионального программиста>, <Book: Грокаем алгоритмы>]>

>>> Book.objects.filter(category=F('id')+1)
<QuerySet [<Book: PostgreSQL. Основы языка SQL>]>

>>> Book.objects.filter(category=F('id')+2)
<QuerySet []>

>>> Book.objects.filter(category=F('id')-1)
<QuerySet [<Book: Кто виноват?>]>

>>> Book.objects.filter(category=F('id')-3)
<QuerySet [<Book: Джанки>]>

>>> Book.objects.filter(category=F('id')-4)
<QuerySet [<Book: Фенэтиламины, которые я знал и любил>]>

Используется он также как класс Q, критерий фильтрации оборачивается в класс F и получается экземпляр класса F.
Рассмотрим вот такой пример, с первого раза может быть трудно сразу сообразить, что тут происходит. Будет легче разобраться, если открыть табличку в БД и посмотреть на ее поля.

Так сразу становится понятней, мы выбираем книги, у которых id либо просто равно значению категории, либо равно с каким-либо математическим условием. Так запись Book.objects.filter(category=F('id')-1) возвращает 'Кто виноват?', потому что id этой книги 4, а id ее категории 3. Это, конечно, искусственный пример и на практике, вряд ли понадобится такая выборка, но класс F и его возможности это пример иллюстрирует хорошо.

objects. Как написать свой менеджер

В самом начале этого блока мы кратенько упомянули классы Manager и QuerySet. Класс QuerySet содержит методы, которые мы применяем к моделям через objects, а сам objects это имя по умолчанию у все моделей для обращения к классу Manager. Мы можем написать свой Manager и обращаться через него к методам QuerySet, но со своим дополнительным функционалом.

book/models.py
class Book(models.Model):
    ...

    objects = models.Manager()
Terminal
>>> Book.objects.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> Book.objects.filter(year__gte=2000)
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Фенэтиламины, которые я знал и любил>]>

Каждая модель по умолчанию содержит такую запись, соответственно вы добавить эту запись к себе в модель и заменить имя objects на какое-нибудь другое и тогда к методам класса QuerySet вы будете обращаться уже через это имя.

book/models.py
...
class CustomManager(models.Manager):
    def year_gte(self):
        return super().get_queryset().filter(year__gte=2000)

...

class Book(models.Model):
    ...

    objects = models.Manager()
    custom_manager = CustomManager()
Terminal
>>> Book.custom_manager.year_gte()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Фенэтиламины, которые я знал и любил>]>

>>> Book.custom_manager.all()
<QuerySet [<Book: Git для профессионального программиста>, <Book: PostgreSQL. Основы языка SQL>, <Book: Грокаем алгоритмы>, <Book: Джанки>, <Book: Кто виноват?>, <Book: Фенэтиламины, которые я знал и любил>]>

Или мы можем написать свой класс для менеджера, который будет унаследован от Manager и внутри нашего класса написать дополнительные методы. Например, добавим метод, который делает то же самое что __gte=2000 и теперь мы можем просто писать его имя вместо прописывания этого условия. При этом, помимо нашего кастомного метода доступны и все остальные методы класса QuerySet.
Написание собственного менеджера может быть полезно чтоб не захламлять кодом queryset методов ваши view, а написать всю особую логику вам нужную сразу в models.py.


Материал будет дополняться...
(скорее всего)

Для отправки комментария необходимо авторизоваться



Комментарии

Здесь пока ничего нет...