CRUD и фильтрация через ORM
Для работы с БД у моделей в Django есть встроенный набор методов. Они наследуются от класса models.Model и поддерживают основные операции по обработке данных в БД: CRUD. Вы знакомы с этой аббревиатурой из урока по SQL
CRUD-операции
- Create:
Model.objects.create() — создание объекта в базе - Read:
Model.objects.get(id=N) — чтение объекта по его ключу - Update:
object.property= 'new value' и потом object.save() — изменение объекта - Delete:
object.delete() — удаление объекта из базы
Сейчас мы разберёмся с основными задачами, которые решает Django ORM. Но перед этим познакомимся с инструментом, который упростит нам тестирование кода.
Python shell
С интерпретатором кода Python, как и со многими другими программами, можно работать через командную строку. Если в консоли выполнить команду $python3 без параметров, то интерпретатор Python запустится в «интерактивном режиме». Теперь можно ввести в командную строку любые скрипты python — и они будут выполняться прямо в терминале. Это похоже на работу командной строки, но вместо команд для работы с файлами выполняется программный код, строчка за строчкой.
Откройте новое окно терминала и посмотрите, как работает python shell.
Символы >>> — это приглашение для ввода команд, то же, что и знак $ в командной строке.
Скопировать кодBASH
$ python3
>>> best_slogan = "Mischief Managed!"
>>> 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)
>>> from posts.models import Post, User
>>> Post.objects.all()
Основы работы с shell разобрали, теперь можно и делами заняться. Все следующие команды выполняйте в shell.
Работаем с проектом
Запрос к базе возвращает специальный объект QuerySet, который содержит список объектов, соответствующих условиям запроса. По запросу .all() мы получили все объекты модели Post.
Чтобы получить определённый объект, можно обратиться к нему по его primary key:
Скопировать кодSHELL
>>> me = User.objects.get(pk=3)
>>> me
<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!")
>>> new.id
39
>>> new.text
"Смотри, этот пост создан через shell!"
>>> new.author
<User: admin>
>>> new.author.username
'admin'
Объект new в момент создания сохранился в базе и получил уникальный id.
Чтобы изменить этот объект, надо присвоить новое значение одному из его полей и вызвать метод save():
Скопировать кодSHELL
>>> new.text = "Смотри, этот пост обновлён!"
>>> 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
>>> 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
>>> Post.objects.filter(text__contains='again')
<QuerySet [<Post: Oops, I did it again!>]>
При запросе указываются именованные параметры функции filter(). Имя параметра состоит из имени поля и суффикса, указывающего, какой оператор применять. Доступные операторы:
- exact — точное совпадение.
«Найти пост, где поле id точно равно 1»
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
- contains — поиск по тексту в поле text.
«Найти пост, где в поле text есть слово "oops" именно в таком регистре»
ORM: Post.objects.filter(text__contains="oops")
SQL: SELECT ... WHERE text LIKE '%oops%';
В большинстве баз данных (например, в MySQL или PostgreSQL) ничего не найдётся: регистр символов не совпадает. В посте админа написано "Oops", а в запросе — "oops".
Однако в нашем проекте установлена СУБД SQLite (Django ставит её по умолчанию), и у неё есть неприятная особенность: она не различает регистр символов нигде, кроме как в кодировке ASCII (а мы все давно уже работаем в UTF-8, ведь в ней есть смайлики 😃).
Итак: в базе SQLite фильтр Post.objects..filter(text__contains="oops") найдёт записи со словами oops, Oops и OOPS, а в большинстве других баз такой запрос вернёт только запись, где регистр слова совпадает полностью.
- in — вхождение в множество.
«Найти пост, где значение поля id точно равно одному из значений: 1, 3 или 4»
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;
- Операторы сравнения с началом и концом строки
startswith, endswith
«Найти посты, где содержимое поля text начинается со строки "Утромъ"»
ORM: Post.objects.filter(text__startswith="Утромъ")
SQL: SELECT ... WHERE text LIKE Утромъ% ESCAPE
- range — вхождение в диапазон
Скопировать код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))
- При работе с частями дат можно применять дополнительные суффиксы date, year, month, day, week, week_day, quarter и указывать для них дополнительные условия:
Скопировать код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.
- isnull — проверка на пустое значение.
ORM: Post.objects.filter(pub_date__isnull=True)
SQL: SELECT ... WHERE pub_date IS NULL;
Объединение условий
В одном запросе можно указать несколько условий одновременно. Для этого последовательно вызовите методы filter() с различными параметрами. Будет сгенерирован SQL-запрос, в котором все условия объединены оператором AND.
Исключить данные из выборки можно методом exclude():
Скопировать кодSHELL
>>> 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 есть и дополнительный синтаксис, он описан в
документации.