#python #django #django-models #django-queryset #django-annotate
Вопрос:
Контекст
Я совсем новичок в Django и пытаюсь написать сложный запрос, который, как мне кажется, будет легко записываться в необработанном SQL, но для которого я изо всех сил использую ORM.
Модели
У меня есть несколько моделей с именами SignalValue
, SignalCategory
, SignalSubcategory
, SignalType
, SignalSubtype
которые имеют ту же структуру, что и следующая модель:
class MyModel(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField()
fullname = models.CharField()
У меня также есть явные модели, которые представляют отношения между моделью SignalValue
и другими моделями SignalCategory
, SignalSubcategory
, SignalType
, SignalSubtype
. Каждое из этих отношений названо SignalValueCategory
, SignalValueSubcategory
, SignalValueType
, SignalValueSubtype
соответственно. Ниже SignalValueCategory
приведена модель в качестве примера:
class SignalValueCategory(models.Model):
signal_value = models.OneToOneField(SignalValue)
signal_category = models.ForeignKey(SignalCategory)
Наконец, у меня также есть две следующие модели. ResultSignal
хранит все сигналы, связанные с моделью Result
:
class Result(models.Model):
pass
class ResultSignal(models.Model):
id = models.BigAutoField(primary_key=True)
result = models.ForeignKey(
Result
)
signal_value = models.ForeignKey(
SignalValue
)
Запрос
Чего я пытаюсь добиться, так это следующего. Для данного Result
я хочу получить все ResultSignal
s , которые ему принадлежат, отфильтровать их, чтобы сохранить те, которые меня интересуют, и аннотировать их двумя полями, которые мы будем называть filter_group_id
и filter_group_name
. Значения двух полей определяются по SignalValue
заданному ResultSignal
значению .
С моей точки зрения, самый простой способ достичь этого-сначала снабдить SignalValue
s соответствующими filter_group_name
и filter_group_id
, а затем соединить полученный результат QuerySet
с ResultSignal
s. Тем не менее, я думаю, что в Django невозможно объединить две QuerySet
группы вместе. Следовательно, я думал, что мы могли бы, возможно, использовать Prefetch
объекты для достижения того, что я пытаюсь сделать, но, похоже, я не в состоянии заставить это работать должным образом.
Код
Теперь я опишу текущее состояние моих запросов.
First, annotating the SignalValue
s with their corresponding filter_group_name
and filter_group_id
. Note that filter_aggregator
in the following code is just a complex filter that allows me to select the wanted SignalValue
s only. group_filter
is the same filter but as a list of subfilters. Additionally, filter_name_case
is a conditional expression ( Case()
construct):
# Attribute a group_filter_id and group_filter_name for each signal
signal_filters = SignalValue.objects.filter(
filter_aggregator
).annotate(
filter_group_id=Window(
expression=DenseRank(),
order_by=group_filters
),
filter_group_name=filter_name_case
)
Затем, пытаясь присоединиться/прокомментировать SignalResult
s:
prefetch_object = Prefetch(
lookup="signal_value",
queryset=signal_filters,
to_attr="test"
)
result_signals: QuerySet = (
last_interview_result
.resultsignal_set
.filter(signal_value__in=signal_values_of_interest)
.select_related(
'signal_value__signalvaluecategory__signal_category',
'signal_value__signalvaluesubcategory__signal_subcategory',
'signal_value__signalvaluetype__signal_type',
'signal_value__signalvaluesubtype__signal_subtype',
)
.prefetch_related(
prefetch_object
)
.values(
"signal_value",
"test",
category=F('signal_value__signalvaluecategory__signal_category__name'),
subcategory=F('signal_value__signalvaluesubcategory__signal_subcategory__name'),
type=F('signal_value__signalvaluetype__signal_type__name'),
subtype=F('signal_value__signalvaluesubtype__signal_subtype__name'),
)
)
Обычно, насколько я понимаю, в результате QuerySet
должно быть поле «тест», которое теперь доступно, которое будет содержать поля signal_filter
первого QuerySet
. Однако Джанго жалуется, что "test"
не найден при вызове .values(...)
в последней части моего кода: Cannot resolve keyword 'test' into field. Choices are: [...]
. Это похоже to_attr
на то, что параметр Prefetch
объекта вообще не учитывался.
Вопросы
- Неужели я неправильно понял функционирование
annotate()
иprefetch_related()
функции? Если нет, то что я делаю не так в своем коде, чтобы указанный параметрto_attr
не существовал в моем результатеQuerySet
? - Есть ли лучший способ объединить два
QuerySet
s в Django или мне лучше использовать RawSql? Альтернативным способом было бы переключиться на Pandas, чтобы выполнить объединение в памяти, но очень часто более эффективно выполнять такие преобразования на стороне SQL с помощью хорошо продуманных запросов.
Ответ №1:
Вы на правильном пути, но просто упускаете то, что делает префетч.
- Ваши аннотации верны, но предварительная выборка «тест» на самом деле не является атрибутом. Вы пакуете
SELECT * FROM signal_value
запросы, поэтому вам не нужно выполнять выбор для каждой строки. Просто отбросьте аннотацию «тест», и все будет в порядке. https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related - Пожалуйста, не используйте панд, в этом определенно нет необходимости, и это накладные расходы. Как вы сами сказали, более эффективно выполнять преобразования на стороне sql
Комментарии:
1. Я не совсем уверен, что понимаю, что вы имеете в виду. Вы имеете в виду, что я должен удалить параметр
to_attr
prefetch_object
и что затем я смогу получить доступ кsignal_value
пользовательским полям с комментариями, как это.values([...], signal_value__filter_group_id)
? Если это так, я протестировал его, но я получаю аналогичную ошибку, которуюgroup_filter_id
невозможно устранить. И что касается того, что «тест» не является атрибутом, он выглядит так, как мне кажется, из документа. Так что я, должно быть, что-то упустил. Не могли бы вы также прояснить эту часть, пожалуйста?2. На самом деле сегодня я наткнулся на аналогичный пример. Хотя это не доступ к префретчу в наборе запросов. Я думаю, вы могли бы перейти к доступу для каждого результирующего сигнала:
for result_signal in result_signals: test = next( iter(result_signal.test), None )
Ответ №2:
Из документов на prefetch_related
:
Помните, что, как всегда в случае с наборами запросов, любые последующие цепные методы, подразумевающие другой запрос к базе данных, будут игнорировать ранее кэшированные результаты и извлекать данные с помощью нового запроса к базе данных.
Это не очевидно, но values()
вызов является частью этих цепных методов, которые подразумевают другой запрос и фактически отменят prefetch_related
его. Это должно сработать, если вы его удалите.
Комментарии:
1. Я попытался удалить
.values()
вызов, но после этого я все еще не могу получить доступ кto_attr="test"
значению. Точнее, я сделал следующее после удаления вызова:result_signals.first().test
. Я думаю, что это должен быть подходящий способ получить доступ к тому, что я хочу, верно? Что касается цепных методов, которые подразумевают другой запрос к базе данных, предполагается, что этот.values()
метод эквивалентен простомуSELECT
предложению в SQL. Какой смысл использовать ORM, если мы даже не можем сделать это в предварительно заданных аннотированных наборах запросов? На самом деле нет смысла делать выбор на Python.2.
result_signals.first()
также подразумевается другой запрос, поэтому то, что вы можете сделать в этом случаеresult_signals.all()[0].test
, описано в документах. О.values()
, это тоже делаетgroup by
А. Я думаю, что вы можете использовать в этом случае.only()
, но я не пробовал, если это также нарушит кэш.3. Поэтому я только что проверил, но , похоже, это не работает лучше :(. Все еще возникает та же ошибка при использовании
result_signals.all()[0].test
, т. Е.'ResultSignal' object has no attribute 'test'
.4. Я вижу, вы можете поделиться всем последним соответствующим кодом, который у вас есть?
5. На самом деле, я поместил код из своего поста в отдельную функцию, чтобы избежать его изменения до тех пор, пока я не найду правильное решение здесь, на SO (а пока в качестве временного решения я использую упомянутую альтернативу Pandas в другой функции, хотя я хорошо осведомлен о накладных расходах). Так что с тех пор обновления кода не было. Дайте мне знать, если есть какая-либо другая информация, которую я мог бы предоставить, чтобы помочь найти источник моей проблемы!