CRUD - основы ORM по работе с моделями

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

На предыдущем занятии мы с вами описали модель Women и создали на ее основе таблицу в БД, используя миграцию. Теперь пришло время научиться работать с этой таблицей, добавлять, выбирать, менять и удалять записи. По-английски эти операции сокращенно записываются как CRUD по первым буквам английских слов:

  • Create – создание;
  • Read – чтение;
  • Update – изменение;
  • Delete – удаление.

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

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

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

То есть, когда мы выбираем записи, или создаем новые записи, то происходит работа именно с объектами класса Women.

Для демонстрации работы с ORM перейдем в консоль Django, из которой будут доступна папка текущего проекта. В терминале выполним команду:

python manage.py shell

чтобы войти в консоль фреймворка. Первым делом выполним импорт модели:

from women.models import Women

Теперь, чтобы создать новую запись в таблице, нам достаточно создать экземпляр класса Women и передать в его конструктор значения именованных параметров, определяющие атрибуты класса и поля таблицы:

Women(title='Анджелина Джоли', content='Биография Анджелины Джоли')

Мы видим, что объект был создан. Но почему были указаны только два параметра? Дело в том, что атрибуты time_create и time_update у нас в модели инициализируются автоматически и определять их конкретными значениями нет необходимости. Поле is_published по умолчанию также принимает значение True, а атрибут photo в консоли Django будет принимать пустую строку, если мы здесь ничего не передаем. По идее, content тоже можно было не определять (стоит параметр blank=True), но я бы хотел, чтобы оно содержало короткую строку.

Если сейчас перейти в SQLiteStudio и посмотреть содержимое таблицы women_women, то никаких записей не увидим. Почему так произошло? Дело в том, что модели в Django по умолчанию являются «ленивыми», создание экземпляра класса еще не означает добавление записи в таблицу. Как вы понимаете, это сделано специально. Мы можем в разных местах программы создавать объекты моделей и только в последний момент запускать их на исполнение, то есть, заносить информацию в БД. Благодаря этому Django имеет возможность оптимизировать SQL-запросы и не нагружать СУБД.

Хорошо, давайте все же укажем фреймворку сохранить созданную запись в таблице. Для этого нам понадобится какая-либо переменная, ссылающаяся на созданный объект, например, такая:

w1 = _

Здесь символ ‘_’ – это специальная ссылка, в которой сохраняется результат последней операции. Если вывести переменную w1, то увидим строку:

<Women: Women object (None)>

Здесь None – это номер id, который принимает определенное, уникальное числовое значение в момент помещения записи в таблицу. Это делается с помощью метода save():

w1.save()

и, выводя опять же эту переменную в консоль, получаем строку:

<Women: Women object (1)>

Видим вместо None уже стоит значение 1, то есть, запись была добавлена непосредственно в таблицу и ей был присвоен идентификатор с номером 1. Перейдем в программу SQLiteStudio и убедимся, что данные в таблице действительно появились. Да, это так, причем остальные поля также были проинициализированы нужными значениями.

Непосредственно в программе, то есть, в консоли Django мы можем оперировать всеми этими данными через ссылку w1:

w1.id # идентификатор
w1.title # заголовок
w1.time_create # время добавления записи

и так по всем полям (атрибутам класса). Помимо этих стандартных атрибутов объекты моделей содержат еще один часто используемый атрибут:

w1.pk # значение primary key

который совпадает с атрибутом id. Зачем было сделано такое дублирование? Дело в том, что поле id в таблицах имеет важное значение: часто именно по нему устанавливаются связи между таблицами. Поэтому по соглашению в Django решили определить атрибут со строго определенным именем pk, который будет всегда доступен и содержать номер текущей записи, либо значение None, если оно не определено. Позже мы не раз будем обращаться к этому свойству, как к идентификатору записи.

Чтобы увидеть SQL-запрос, который был выполнен для добавления этой записи, нам нужно сначала импортировать модуль connection:

from django.db import connection

и обратиться к коллекции queries:

connection.queries

В консоли увидим список словарей из выполненных запросов. У этого словаря имеются два ключа: sql – это текст SQL-запроса; time – это время выполнения этого запроса.

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

w2 = Women(title='Энн Хэтэуэй', content='Биография Энн Хэтэуэй')

Список queries  остался прежним, так как запись еще не была добавлена в таблицу. Выполним команду:

w2.save()

Теперь в списке queries два запроса, а в таблице две записи. Эти два последних примера показывают, что объект класса Women можно наполнять информацией как угодно до момента непосредственной записи. Например, можно вначале создать экземпляр класса без параметров:

w3 = Women()

а, затем, его атрибутам присвоить требуемые значения:

w3.title = 'Джулия Робертс'
w3.content = 'Биография Джулии Робертс'

После вызова метода save, запись будет добавлена в таблицу:

w3.save()

Менеджер записей objects

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

Women.objects

В консоли увидим:

<django.db.models.manager.Manager object at 0x0399CA00>

Что делает этот объект objects, который еще называют менеджером записей? У него есть несколько весьма полезных методов. Начнем с метода добавления create():

w4 = Women.objects.create(title='Ума Турман', content='Биография Ума Турман')

Если теперь обратиться к свойству:

w4.pk

то увидим значение 4, то есть, запись была автоматически добавлена в БД. Нам здесь не нужно отдельно вызывать метод save(), все происходит «на лету», что бывает весьма полезно. Давайте добавим еще одну запись, не присваивая результат какой-либо переменной:

Women.objects.create(title='Кира Найтли', content='Биография Киры Найтли')

В таблице появилась пятая запись с идентификатором 5. Вот так довольно просто можно добавлять новые записи в таблицы, используя ORM Django.

Выборка записей из таблицы

Как теперь можно прочитать данные из таблицы women? Для этого можно воспользоваться менеджером записей – объектом objects и выполнить метод all():

Women.objects.all()

На выходе мы получаем список объекта QuerySet:

<QuerySet [<Women: Women object (1)>, <Women: Women object (2)>, <Women: Women object (3)>, <Women: Women object (4)>, <Women: Women object (5)>]>

Но вот такая информация, когда показываются записи с идентификаторами не очень информативна. Давайте вместо идентификаторов будем выводит заголовок – поле title. Для этого в модели достаточно переопределить магический метод __str__:

class Women(models.Model):
...
    def __str__(self):
        return self.title

Чтобы изменения вступили в силу, выйдем из консоли Django (команда exit()) и снова в нее зайти (команда python manage.py shell). Импортируем модель:

from women.models import Women

И выполняем команду выбора всех записей:

Women.objects.all()

Теперь в консоли отображаются заголовки записей, а не их id:

<QuerySet [<Women: Анджелина Джоли>, <Women: Энн Хэтэуэй>, <Women: Джулия Робертс>, <Women: Ума Турман>, <Women: Кира Найтли>]>

Как из полученного списка получить отдельную запись? Это можно сделать либо по индексу, например:

w = _
w[0]

Причем, w[0] – это ссылка на объект класса Women, у которого имеются указанные нами атрибуты. Эти атрибуты содержат информацию о текущей записи:

w[0].title
w[0].content

Общее число записей определить с помощью стандартной функции len:

len(w)

Также этот список можно перебрать циклом for, например, так:

for wi in w:
     print(wi.title)

Как вы понимаете, если таблица будет состоять из тысяч, а то и миллиона записей, то все они будут возвращаться методом all(). (Хотя в консоли стоит ограничение на 21 запись.) Это не очень хорошо с точки зрения расхода память и ресурсов процессора. Обычно в программе нам требуется всего несколько записей, выбранных по какому-либо критерию (условию). Для этого вместо метода all() следует использовать метод filter(), например, так:

Women.objects.filter(title='Энн Хэтэуэй')

На выходе получим одну запись, у которой id равен 2. Если посмотреть SQL-запрос, который был выполнен:

from django.db import connection
connection.queries

то он окажется следующим:

'SELECT "women_women"."id", "women_women"."title", "women_women"."content", "women_women"."photo", "women_women"."time_create", "women_women"."time_update", "women_women"."is_published" FROM "women_women" WHERE "women_women"."title" = \'Энн Хэтэуэй\' LIMIT 21'

То есть, метод filter использует оператор WHERE для формирования выборки. Если условию не будет удовлетворять ни одна запись, то получим пустой список:

Women.objects.filter(title='Энн')

Этот пример также показывает, что ищется строка целиком, а не ее часть.

Что если мы хотим сделать выборку по записям, у которых id больше или равен 2. Записывать условие в виде:

Women.objects.filter(pk > 2)

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

  • <имя атрибута>__gte – сравнение больше или равно (>=);
  • <имя атрибута>__lte – сравнение меньше или равно (<=).

В результате, искомый фильтр можно записать так:

Women.objects.filter(pk__gte=2)

Противоположный по действию является метод exclude(). Он выбирает записи не удовлетворяющие указанному условию. Например:

Women.objects.exclude(pk=2)

Будут выбраны все записи, кроме записи с id равным 2.

Итак, методы filter и exclude позволяют выбирать совокупность записей по определенному условию. Но что если нам нужно выбрать только одну, строго определенную запись обычно, используя, ее идентификационный номер id. Конечно, здесь мы также можем использовать метод filter, например, так:

Women.objects.filter(pk=2)

Получим запись с id равным 2. Но в ORM в этом случае следует использовать другой метод get(), заточенный специально для этой цели. Запишется он аналогично:

Women.objects.get(pk=2)

В чем разница между этими двумя методами? Смотрите, если по условию записей будет несколько:

Women.objects.get(pk__gte=2)

или они не будут существовать:

Women.objects.get(pk=20)

то метод get() генерирует исключения. А метод filter() вернет несколько записей или пустой список. То есть, используя метод get() мы уверены, что была получена только одна запись и в таблице по указанному условию других записей нет. Часто это бывает очень важно, например, при авторизации пользователя мы должны найти одну уникальную запись, связанную именно с ним и никакую другую. В этом случае метод get() незаменим.

Сортировка записей

Если при выборке нам нужно добавить порядок сортировки наших записей по какому-либо полю, то можно воспользоваться методом order_by(), например:

Women.objects.filter(pk__lte=4).order_by('title')

Выберем все записи, у которых id меньше или равен 4 и отсортированных по полю title в порядке возрастания (используется лексикографическое сравнение строк). Этот пример показывает как методы можно цепочкой выполнять друг за другом: сначала выбрали записи по условию, а затем отсортировали их. Точно также по цепочке можно выполнять все другие методы ORM.

Также можно сразу записать метод order_by():

Women.objects.order_by('title')

тогда будут выбраны все записи с той же самой сортировкой.

Если нам нужно изменить порядок сортировки на противоположный, то перед именем поля достаточно поставить знак минус:

Women.objects.order_by('-time_update')

Изменение записей

В самом простом случае, для изменения какой-либо записи, ее можно сначала прочитать из БД, например, с помощью метода get():

wu = Women.objects.get(pk=2)

А, затем, присвоить атрибутам объекта Women другие значения:

wu.title = 'Марго Робби'
wu.content = 'Биография Марго Робби'

Сохраняем новые данные:

wu.save()

и в таблице видим, что вторая запись содержит новую, измененную информацию. Если мы посмотрим на последний SQL-запрос:

connection.queries

то он будет следующий:

'UPDATE "women_women" SET "title" = \'Марго Робби\', "content" = \'Биография Марго Робби\', "photo" = \'\', "time_create" = \'2021-01-03 09:27:56.511898\', "time_update" = \'2021-01-03 11:57:06.800768\', "is_published" = 1 WHERE "women_women"."id" = 2'

Удаление записей

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

wd = Women.objects.filter(pk__gte=4)

А, затем, выполняем для них метод delete():

wd.delete()

Все, записи с id равным 4 и 5 были удалены из таблицы women.

На этом занятии мы лишь в целом рассмотрели некоторые возможности ORM Django. Более детальную информацию можно почитать на странице вот этой русскоязычной документации:

https://djbook.ru/rel3.0/topics/db/queries.html

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

Видео по теме