Одна из задач, которую берут на себя ORM – это построение произвольных SQL-запросов в базу данных. Вот как это делается с помощью Django ORM:
# SELECT * FROM users WHERE first_name = "John" ORDER BY last_name DESC LIMIT 10
users = User.objects.filter( # WHERE
first_name='John',
).order_by( # ORDER BY
'-last_name'
)[:10] # LIMIT
for user in users:
print(user.last_name)
Менеджер и Запросы
На текущем этапе важно познакомиться с двумя терминами, которые используются повсеместно, когда речь идёт о построении запросов в Django ORM: Менеджер (Manager) и Запрос (QuerySet). Manager отвечает за связь модели как Python-класса с фактическим представлением данных в соответствующей таблице БД, а также позволяет программисту строить запросы (QuerySets) к этой таблице и создавать в ней новые записи. Помимо построения запросов, менеджер интерпретирует их результаты. Например, представляет данные из базы в виде экземпляров класса модели. У любой модели есть хотя бы один менеджер — тот, что доступен через атрибут .objects
класса модели. В примере выше User.objects
— менеджер модели User
.
С методом .create()
менеджера вы уже встречались на предыдущем уроке. Этот метод создает новые записи в таблице. Большинство же других методов начинают построение запроса — значения типа QuerySet
. Каждый подобный метод возвращает новый QuerySet, запоминающий новые параметры будущего запроса. В примере цепочка вызовов .filter(..).order_by(..)
конструирует запрос по шагам. И даже взятие среза ([:10]
) только уточняет запрос, также создавая новый QuerySet
.
Обратите внимание: сам SQL-запрос сразу не выполняется. Для того, чтобы выполнить SQL-запрос или, как говорят, "финализировать", нужно начать итерацию результатов или обратиться к одному из "элементов" имеющегося QuerySet по индексу. В примере SQL-запрос выполняется в тот момент, когда цикл for
пробует получить от users
первый элемент.
Построение запросов с помощью методов
Пока запрос не финализирован, его можно свободно сохранять в переменные или передавать из одного участка кода в другой и строить новые QuerySets на основе сохранённого:
johns = User.objects.filter(first_name='John') # все Джоны
connors = johns.filter(last_name='Connor') # только Джоны Конноры
others = johns.exclude(last_name='Connor') # Джоны, но не Конноры
Здесь представлено три QuerySets, причём второй и третий построены на основе первого. И ни один из них ещё не финализирован.
Обратите внимание: первый вызов в цепочке всегда относится к менеджеру, каким бы ни был метод
Порядок вызова методов не важен. Сначала можно сортировать, а потом фильтровать:
users = User.objects.order_by(
'-last_name', # "-" означает "по убыванию"
).filter(
first_name='John',
)
# SELECT * FROM users WHERE first_name = "John" ORDER BY last_name DESC
Django ORM самостоятельно расставит все части в правильном порядке. Однако, несмотря на такую заботу, всё же рекомендуется везде, где это возможно, соблюдать ожидаемый порядок вызовов. Это упростит чтение кода.
Многие вызовы могут накапливаться. Цепочка из filter
породит в SQL-запросе одну часть WHERE
, где все условия объединены с помощью оператора AND
:
User.objects.filter(first_name='John').filter(age=34).exclude(city='Moscow')
# WHERE first_name = "John" AND age = 34 AND NOT (city = "Moscow")
Но некоторые методы не накапливаются. Например, каждый последующий вызов .order_by()
заменяет ранее указанное правило сортировки на новое! Это позволяет описать и сохранить сортировку по умолчанию, оставив возможность её подменить, если понадобится.
В языке запросов Django ORM есть по методу на каждую часть SQL. Некоторые из них в таблице ниже:
метод | SQL |
---|---|
.filter() , .exclude() |
WHERE |
.order_by() |
ORDER |
[:] |
LIMIT /OFFSET |
Мы рассмотрим только некоторые из них. Остальные достаточно легко понять из документации, если вы знаете SQL. Если это не так, рекомендуем пройти курс основы баз данных.
.filter()
— наиболее часто используемая операция при построении запросов. Метод .exclude()
во всём похож на .filter()
и лишь добавляет NOT
к условию, то есть инвертирует оное. Взглянем на примеры фильтрации:
# WHERE votes = 100
User.objects.filter(votes=100)
# WHERE votes >= 100
User.objects.filter(votes__gte=100)
Обратите внимание на то, как закодировано нестрогое равенство: "__gte" — двойное подчёркивание плюс сокращение от "greater than or equal". В Django условия принято указывать в виде именованных аргументов, а в их именах нельзя использовать символы вроде "<
", поэтому потребовались сокращения. Двойное подчёркивание однозначно отделяет имя поля от оператора — его ещё называют "lookup". А двойное оно потому, что имена полей могут содержать одиночные символы подчёркивания ("last_name").
Помимо проверок на неравенство, таких как __gte
, Django поддерживает множество других lookups. Вот некоторые из них:
User.objects.filter(
id__in=[45, 101, 512], # равно одному из значений списка (входит В список)
first_name__iexact='John', # равно без учёта регистра
age__range=(18, 21), # входит в диапазон 18..21
middle_name__isnull=True, # необязательное поле не заполнено
last_name__icontains='ibn' # содержит подстроку без учёта регистра
)
Менеджеры и итерация
Выше было сказано, что QuerySet
превращается в SQL-запрос при обращении к его элементам по индексу или при запуске процесса итерации. К менеджеру это, увы, не относится и для того, чтобы сделать запрос, нужен хотя бы какой-нибудь QuerySet
.
Именно поэтому периодически возникают ошибки вроде "'Manager' is not iterable" или "'Manager' is not subscriptable" в процессе выполнения кода вида for u in User.objects: ..
или User.objects[0]
. Как только вы вызовите хотя бы один метод, уточняющий запрос, вы получите в ответ QuerySet
и проблема решится сама собой. Взятие среза к менеджеру, увы, не применимо, потому что это подвид обращения по индексу (subscription protocol).
Если же вам не нужно уточнять запрос и вы действительно хотите работать со всеми записями, то используйте метод .all()
, который для менеджера вернёт QuerySet
без дополнительных условий. Если же вызвать .all()
у имеющегося QuerySet
, то тот просто "вернёт себя". Так что бояться метода .all()
не следует, его вызов вовсе не означает "запросить всё"!
Зачем?
Для чего нужен такой язык, почему недостаточно SQL? На это есть несколько разных причин:
- Универсальность. Django ORM способна генерировать SQL, подходящий под конкретную базу данных. Построение запросов же не привязано к базе данных. Хотя это не отменяет ситуаций, в которых приходится выполнять "сырые" запросы в базу данных.
- Безопасность. Такой способ построения запросов автоматически экранирует все подставляемые значения.
- Автоматическая конвертация. Если делать запросы руками, то придётся руками же описывать как выбранные данные должны лечь на свойства конкретной модели. Это довольно серьёзная работа, которую лучше поручить ORM (во многом она для этого и создавалась).
- Динамические запросы. SQL очень плохо подходит для динамических запросов, когда они конструируются по условиям. Такое часто встречается в фильтрах.
Самостоятельная работа
Откройте REPL. Попробуйте повыбирать объекты имеющихся моделей по тем или иным условиям.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.