Демистификация сложных запросов для Django ORM
Мотивация
Итак, я работал над созданием своего личного проекта, когда я натолкнулся на препятствие при написании определенного запроса. Я хорошо разбираюсь в SQL как таковом, но я почувствовал себя растерянным, когда попытался преобразовать эту логику в классическое объектно-реляционное сопоставление (ORM) Django. Это заставило меня пройти через несколько болезненных часов, пока один джентльмен наконец не ответил на мой конкретный вопрос о переполнении стека. После этого ответа я решил просмотреть несколько ресурсов и аккуратную документацию Django, чтобы составить небольшое руководство по созданию вашей сложной логики SQL в рамках предоставленной ORM.
Вступление
Эта статья предполагает базовые знания Django и основы SQL. Основные темы, которые я стремлюсь охватить: агрегирование, функции, подзапросы, оконные функции. Я стремлюсь добавить больше концепций, поскольку чувствую личную потребность в их использовании. Эмпирическое правило: любое выражение, которое является более сложным или требует больше времени, чем ваша доступная пропускная способность? Просто напишите необработанный SQL в курсор Django. Это не лучший метод, так как это довольно уязвимые SQL-инъекции. Тем не менее, при соблюдении надлежащих правил и функционального программирования атаки с использованием инъекций также могут быть защищены.
В основном я буду говорить о модели вопроса и модели ответа, схема которой определена на самом верхнем изображении выше.
Агрегирование
Как и в обычном SQL, агрегирование может выполняться либо для получения сводного результата по всему набору запросов, либо для получения индивидуального отчета для каждого объекта в наборе запросов.
Aggregate()
Предположим, у вас есть модель вопроса, в которой каждый вопрос поддерживает поле ManyToMany
с пользователем, обозначающее отношения «Follower». Если мне нужна сводка голосов, полученных за вопрос, я могу выполнить следующий запрос:
>>> from django.db.models import Count >>> Question.objects.aggregate( avg_followers = Avg(Count('followers')), highest_followers = Max(Count('followers')) lowest_followers = Min(Count('followers')) ){'avg_followers' : 14.23, 'highest_followers' : 1523, 'lowest_followers' : 0}
Обратите внимание, как мне пришлось импортировать функцию Count
и заключить в нее поле «followers». Followers - это ManyToManyField
, функция Count
возвращает количество пользователей, которые подписались на вопрос. На основе этого результата по полному набору запросов находятся соответствующие минимальные, максимальные и средние подписчики.
Annotate()
Как вкратце объяснено выше, аннотация используется для создания индивидуальной сводки для каждого объекта в наборе запросов. Это означает, что результат агрегирования вычисляется для каждой отдельной записи, которая может быть или не совпадать. Если бы я хотел создать счетчик «за» для всех проголосовавших за ответ в модели ответа:
>>> from django.db.models import Count >>> query = Answers.objects.annotate(upvotes = Count('upvoters')) >>> query[0] <Answer : 1> >>> query[0].upvotes 143 >>> query[1] <Answer : 2> >>> query[1].upvotes 23
F() выражения
Выражение F()
представляет значение поля/аннотированного столбца модели. Он используется для эффективного обращения к значениям полей модели без фактического извлечения их в память Python. В результате работу выполняет база данных, а не Python.
>>> from django.db.models import F >>> query = Answer.objects.get(author__username='keshavvinayak01') >>> query.answer Yes, Math is in fact related to Science. >>> query.update(answer = F('answer') + " [Anonymous]") >>> query.answer Yes, Math is in fact related to Science. [Anonymous]
Мы можем использовать эти выражения в аннотациях, фильтрах или ссылках на внешний ключ.
>>> from django.db.models import F # В аннотациях >>> answer = Answer.objects.annotate( interest = Count(F('followers') + Count(F('requested') ) # В фильтрах >>> own_qa = Answer.objects.filter( question__asker = F('author') ) # Ссылка на внешний ключ >>> answer = Answer.objects.annotate( question_id = F('question') ) >>> answer.question Is Math related to Science? >>> answer.question_id 3
Func() выражения
Эти выражения включают функции базы данных, такие как LOWER или COALESCE. Вот несколько примеров:
LOWER >>> from django.db.models import F, Func, Coalesce >>> quest = Question.objects.annotate( lower_question = Func(F('question')), function = 'LOWER' ) # Все тексты вопросов будут в нижнем регистре. COALESCE >>> from datetime import datetime >>> q = Answer.objects.annotate( updated_at = Coalesce(F('updated_at'), datetime.now()) )
В первом результате запроса TextField
вопроса преобразуется в нижний регистр. Это эквивалентно выбору LOWER (question.question)
в SQL. Во втором запросе мы просто устанавливаем значение поля updated_at
на текущую дату и время, если оно равно нулю, иначе просто получаем его.
Subquery() выражения
Мы можем добавить явный подзапрос в наш набор запросов, используя выражения Subquery()
. Например, если мы хотим получить вопрос только с ответом, получившим наибольшее количество голосов, нам нужно будет включить результат подзапроса, выбирающего такой ответ.
>>> from django.db.models import OuterRef, Subquery >>> answer_subquery = Answer.objects.filter( question=OuterRef('pk')).annotate( upvotes=Count(F('upvoters') ) ).order_by('-upvotes')# What's happening here? >>> questions = Questions.objects.annotate( answer=Subquery(answer_subquery.values('answer')[-1]) )
Давайте разберемся с этим до необработанного SQL. Мы хотели получить каждый вопрос вместе с ответами, получившими наибольшее количество голосов. Итак, сначала мы создали подзапрос, который извлекает каждый ответ на заданный вопрос, указанный в OuterRef, а затем сортирует их в порядке убывания в зависимости от количества полученных ими голосов.
Во второй части мы добавляем аннотированное поле ответа, которое в основном является результатом подзапроса, ограниченного 1, чтобы получить ответ с наибольшим количеством голосов.
Для положительных голосов ведется отдельная таблица. Можно легко подсчитать группировку по ответам. Для простоты этот запрос пока можно проигнорировать.
SQL может выглядеть примерно так:
# Отсутствуют некоторые подробности о проголосовавших, так как это поле "многие ко многим". SELECT "question"."id", ( SELECT "answer"."answer",COUNT("answer.upvoters") as "upvotes" FROM "answer" WHERE "answer"."question" = "question"."id" GROUP BY 1 ORDER BY COUNT("answer.upvoters") DESC LIMIT 1 ) as "answer" from "question"
Помимо этих основных концепций, вы можете самостоятельно писать взаимосвязанные запросы, если поняли все вышеперечисленное. Вы можете создавать агрегаты внутри подзапросов, подзапросов с фильтрами и т.д.
Иногда вам может потребоваться выполнить запросы, которые не точно соответствуют моделям, или напрямую выполнить запросы на обновление, вставку или удаление. В этом случае вы можете получить прямой доступ к базе данных, полностью отклонившись от слоя модели. Пример:
from django.db import connection
def my_custom_sql(self):
with connection.cursor() as cursor:
cursor.execute("""
UPDATE x
SET a = 1
WHERE bar = %s
""",[self.a])
cursor.execute("""
SELECT foo
FROM x
WHERE bar = %s
""", [self.a])
row = cursor.fetchone()
return row
Объект django.db.connection
представляет соединение с базой данных по умолчанию. Чтобы использовать соединение с базой данных, вызовите connection.cursor()
, чтобы получить объект курсора. Затем вызовите cursor.execute (SQL, <params>)
для выполнения SQL и cursor.fetchone()
или cursor.fetchall()
для возврата результирующих строк.
Заключение
В этом кратком руководстве мы рассмотрели некоторые основы, которые являются ключевыми для выполнения сложных взломов SQL, а также, наконец, обсудили, как мы можем выполнять пользовательский SQL. Я настоятельно рекомендую просмотреть официальную документацию Django, чтобы получить больше информации о других доступных функциях и приемах, которые предлагает Django ORM, если эта статья не может что-то объяснить. Надеюсь, это помогло, пожалуйста, оставляйте отзывы в случае каких-либо сомнений или отзывов. Спасибо за прочтение!
Вернуться на верх