CRUD и фильтрация через ORM

Для работы с БД у моделей в Django есть встроенный набор методов. Они наследуются от класса models.Model и поддерживают основные операции по обработке данных в БД: CRUD. Вы знакомы с этой аббревиатурой из урока по SQL

CRUD-операции

Сейчас мы разберёмся с основными задачами, которые решает Django ORM. Но перед этим познакомимся с инструментом, который упростит нам тестирование кода.

Python shell

С интерпретатором кода Python, как и со многими другими программами, можно работать через командную строку. Если в консоли выполнить команду $python3 без параметров, то интерпретатор Python запустится в «интерактивном режиме». Теперь можно ввести в командную строку любые скрипты python — и они будут выполняться прямо в терминале. Это похоже на работу командной строки, но вместо команд для работы с файлами выполняется программный код, строчка за строчкой.
Откройте новое окно терминала и посмотрите, как работает python shell.
Символы >>> — это приглашение для ввода команд, то же, что и знак $ в командной строке.
Скопировать кодBASH
# запускаем интерпретатор без параметров $ python3 # дальше пишем на python # создаём переменную >>> best_slogan = "Mischief Managed!" # вызываем функцию print() >>> print(best_slogan) # и получаем результат Mischief Managed! # арифметика тоже работает >>> 2 + 2 * 2 6
Прелесть этого режима в том, что он тут же выводит результат выполнения скрипта. Например, создадим и выведем переменную:
Скопировать кодSHELL
>>> x = 5 >>> x 5
Обратите внимание: переменные, которые вы создали во время работы в python shell, будут доступны до тех пор, пока вы не закроете окно терминала.

Django Python shell

В таком же интерактивном режиме можно работать и с Django-проектами. Для этого python shell надо запустить в виртуальном окружении проекта.
Откройте терминал, убедитесь, что запущено виртуальное окружение проекта Yatube и выполните команду:
Скопировать кодBASH
(venv) $ python manage.py shell
Вы увидите примерно такой результат:
Скопировать кодBASH
(venv) $ python manage.py shell Python 3.8.0 (default, Nov 22 2019, 23:37:58) [Clang 11.0.0 (clang-1100.0.33.12)] on darwin Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>>
Всё, что вы хотите узнать, но стесняетесь спросить — выясняйте через команду help.
При работе в интерактивном режиме в Django вам становятся доступны все данные проекта. Можно создавать объекты, управлять базой данных, тестировать функции проекта.
Скопировать кодSHELL
(venv) $ python manage.py shell Python 3.8.0 (default, Nov 22 2019, 23:37:58) [Clang 11.0.0 (clang-1100.0.33.12)] on darwin Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) # чтобы убедиться, что мы работаем именно с нашим проектом Yatube # импортируем модели из проекта и заглянем в базу данных: # запросим все объекты модели Post >>> from posts.models import Post, User >>> Post.objects.all()
Основы работы с shell разобрали, теперь можно и делами заняться. Все следующие команды выполняйте в shell.

Работаем с проектом

Запрос к базе возвращает специальный объект QuerySet, который содержит список объектов, соответствующих условиям запроса. По запросу .all() мы получили все объекты модели Post.
Чтобы получить определённый объект, можно обратиться к нему по его primary key:
Скопировать кодSHELL
# можно использовать User.objects.get(id=1) >>> me = User.objects.get(pk=3) >>> me # python shell сообщает, что переменная me содержит <Объект> класса User, # а поле username этого объекта равно "admin" <User: admin>
Класс User предустановлен в Django, и для него настроен вывод на экран именно в таком виде. При создании любого класса можно описать, каким образом объекты этого класса будут выводиться на экран (например, в python shell или при вызове print(any_object)). Это описывается в «магическом методе» __str__.
Запросим пользователя с pk=13: у нас в базе такой записи пока что нет. Если объект с запрошенным ключом не найден, то появится сообщение об ошибке:
Скопировать кодSHELL
>>> User.objects.get(pk=13) Traceback (most recent call last): File "<console>", line 1, in <module> File "/Dev/Yatube/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/Dev/Yatube/venv/lib/python3.8/site-packages/django/db/models/query.py", line 415, in get raise self.model.DoesNotExist( django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.
Новый объект в базе можно создать методом create():
Скопировать кодSHELL
# создаём объект, передаём свойства >>> new = Post.objects.create(author=me, text="Смотри, этот пост я создал через shell!") # посмотрим, какой id присвоен этому объекту в базе >>> new.id 39 # а что в поле text? >>> new.text "Смотри, этот пост создан через shell!" # а что в поле author? >>> new.author <User: admin> # смотрим, что записано в поле username того объекта, на который ссылается поле author >>> new.author.username 'admin'
Объект new в момент создания сохранился в базе и получил уникальный id.
Чтобы изменить этот объект, надо присвоить новое значение одному из его полей и вызвать метод save():
Скопировать кодSHELL
# присваиваем новое значение полю text >>> new.text = "Смотри, этот пост обновлён!" # но пока что значение изменено лишь в коде. В БД всё ещё хранится старое значение # чтобы отправить новое значение в базу данных — вызываем метод save() >>> new.save()
Если вы изменили объект в коде — он не изменится в базе до тех пор, пока вы не вызовете метод save().
Теперь обновлённый текст записи можно увидеть в админ-зоне.
Удалить объект из базы можно методом delete(). При вызове этот метод дополнительно удалит и все связанные объекты, для которых был задан параметр on_delete=models.CASCADE.
Скопировать кодSHELL
>>> new.delete()
Для дальнейшей работы нам понадобятся тестовые посты админа, создайте их:
Скопировать кодSHELL
>>> first_post = Post.objects.create(author=me, text="Oops, I did it again!") >>> second_post = Post.objects.create(author=me, text="Утромъ гольдъ Дерсу Узала на повторно заданный вопросъ согласенъ ли онъ поступить проводникомъ изъявилъ свое согласіе и съ этого момента онъ сталъ членом экспедиціи")

Фильтрация объектов

Основная задача при работе с базой — это поиск объектов по заданным признакам. В SQL за это отвечают команды блока WHERE, в Django ORM — метод filter():
Скопировать кодSHELL
# найти все объекты, значение поля author у которых равно me # в этой переменной хранится объект User с pk=3 >>> Post.objects.filter(author=me) <QuerySet [<Post: Oops, I did it again!>, <Post: Утромъ гольдъ Дерсу Узала...>]>
В базе найдены две записи, соответствующие условиям запроса.
Если ваш вывод выглядит так <QuerySet [<Post: Post object(39)>, <Post: Post object(40)>] значит в классе Post не хватает метода __str__ , который нужен для красивого вывода объектов на экран. Добавьте его:
Скопировать кодPYTHON
class Post: ... def __str__(self): # выводим текст поста return self.text
Увидеть SQL-запрос, который будет отправлен к базе, можно с помощью команды .query:
Скопировать кодSHELL
>>> print(Post.objects.filter(author=me).query) SELECT "posts_post"."id", "posts_post"."text", "posts_post"."pub_date", "posts_post"."author_id", "posts_post"."group_id" FROM "posts_post" WHERE "posts_post"."author_id" = 1
В Django ORM аналог команд WHERE выглядит так: указывается имя поля, затем два знака подчеркивания __, название фильтра и его значение:
Скопировать кодSHELL
# найти посты, где поле text__содержит строку "again" >>> Post.objects.filter(text__contains='again') <QuerySet [<Post: Oops, I did it again!>]>
При запросе указываются именованные параметры функции filter(). Имя параметра состоит из имени поля и суффикса, указывающего, какой оператор применять. Доступные операторы:
ORM: Post.objects.filter(id__exact=1) или Post.objects.filter(id=1)
На SQL это условие выглядит так: SELECT ... WHERE id = 1.
Сравнение работает и с None. Выражение Post.objects.filter(text=None) превратится в SELECT ... WHERE text IS NULL
ORM: Post.objects.filter(text__contains="oops")
SQL: SELECT ... WHERE text LIKE '%oops%';
В большинстве баз данных (например, в MySQL или PostgreSQL) ничего не найдётся: регистр символов не совпадает. В посте админа написано "Oops", а в запросе — "oops".
Однако в нашем проекте установлена СУБД SQLite (Django ставит её по умолчанию), и у неё есть неприятная особенность: она не различает регистр символов нигде, кроме как в кодировке ASCII (а мы все давно уже работаем в UTF-8, ведь в ней есть смайлики 😃).
Мануалы формулируют проблему так: SQLite only understands upper/lower case for ASCII characters by default.
Итак: в базе SQLite фильтр Post.objects..filter(text__contains="oops") найдёт записи со словами oops, Oops и OOPS, а в большинстве других баз такой запрос вернёт только запись, где регистр слова совпадает полностью.
ORM: Post.objects.filter(id__in=[1, 3, 4])
SQL: SELECT ... WHERE id IN (1, 3, 4);
Если вместо списка будет передана строка, она разобьётся на символы: «Найти пост, где значение поля text точно равно "o", "p" или "s"»
ORM: Post.objects.filter(id__in="oops")
SQL: SELECT ... WHERE text IN ('o', 'p', 's');
gt> (больше),
gte=> (больше или равно),
lt< (меньше),
lte<= (меньше или равно).
«Найти пост, где значение поля id больше пяти»
ORM: Post.objects.filter(id__gt=5)
SQL: SELECT ... WHERE id>5;
«Найти посты, где содержимое поля text начинается со строки "Утромъ"»
ORM: Post.objects.filter(text__startswith="Утромъ")
SQL: SELECT ... WHERE text LIKE Утромъ% ESCAPE
Скопировать кодPYTHON
import datetime start_date = datetime.date(1890, 1, 1) end_date = datetime.date(1895, 3, 31) Post.objects.filter(pub_date__range=(start_date, end_date)) # SQL: SELECT ... WHERE pub_date BETWEEN '1890-01-01' and '1895-03-31'; # выберет посты, опубликованные в диапазоне с 1 января 1890 до 31 марта 1895
Скопировать кодPYTHON
# условия для конкретной даты Post.objects.filter(pub_date__date=datetime.date(1890, 1, 1)) Post.objects.filter(pub_date__date__lt=datetime.date(1895, 1, 1)) # условия для года и месяца Post.objects.filter(pub_date__year=1890) Post.objects.filter(pub_date__month__gte=6) # условия для квартала Post.objects.filter(pub_date__quarter=1)
Такой же синтаксис применяется и для времени: hour, minute, second.
ORM: Post.objects.filter(pub_date__isnull=True)
SQL: SELECT ... WHERE pub_date IS NULL;

Объединение условий

В одном запросе можно указать несколько условий одновременно. Для этого последовательно вызовите методы filter() с различными параметрами. Будет сгенерирован SQL-запрос, в котором все условия объединены оператором AND.
Исключить данные из выборки можно методом exclude():
Скопировать кодSHELL
# выбрать посты, начинающиеся со слова "Утромъ" # исключить из выборки посты автора me # и показать только те посты, которые опубликованы не ранее 30 января 1895 года >>> Post.objects.filter( ... text__startswith='Утромъ' ... ).exclude( ... author=me ... ).filter( ... pub_date__gte=datetime.date(1895, 1, 30) ... )

Сортировка и ограничение количества результатов

Этот синтаксис вам знаком: мы применяли сортировку во view-функции index() и там же ограничили число возвращаемых результатов запроса.
order_by("-pub_date") — сортировать результаты по полю pub_date в обратном порядке (от больших значений к меньшим) [:11] — вернуть не более одиннадцати результатов из найденных.
Скопировать кодSHELL
>>> print(Post.objects.order_by("-pub_date")[:11].query) SELECT "posts_post"."id", "posts_post"."text", "posts_post"."pub_date", "posts_post"."author_id", "posts_post"."group_id" FROM "posts_post" ORDER BY "posts_post"."pub_date" DESC LIMIT 11
Сортировку и ограничение числа возвращаемых результатов можно объединить с фильтрацией:
Скопировать кодSHELL
>>> Post.objects.filter(text__startswith='Утромъ').order_by("-pub_date")[:2]
В Django ORM есть и дополнительный синтаксис, он описан в документации.