Django: Как «соединить» два набора запросов с помощью объекта предварительной выборки?

#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 объекта вообще не учитывался.

Вопросы

  1. Неужели я неправильно понял функционирование annotate() и prefetch_related() функции? Если нет, то что я делаю не так в своем коде, чтобы указанный параметр to_attr не существовал в моем результате QuerySet ?
  2. Есть ли лучший способ объединить два QuerySet s в Django или мне лучше использовать RawSql? Альтернативным способом было бы переключиться на Pandas, чтобы выполнить объединение в памяти, но очень часто более эффективно выполнять такие преобразования на стороне SQL с помощью хорошо продуманных запросов.

Ответ №1:

Вы на правильном пути, но просто упускаете то, что делает префетч.

  1. Ваши аннотации верны, но предварительная выборка «тест» на самом деле не является атрибутом. Вы пакуете SELECT * FROM signal_value запросы, поэтому вам не нужно выполнять выбор для каждой строки. Просто отбросьте аннотацию «тест», и все будет в порядке. https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related
  2. Пожалуйста, не используйте панд, в этом определенно нет необходимости, и это накладные расходы. Как вы сами сказали, более эффективно выполнять преобразования на стороне 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 в другой функции, хотя я хорошо осведомлен о накладных расходах). Так что с тех пор обновления кода не было. Дайте мне знать, если есть какая-либо другая информация, которую я мог бы предоставить, чтобы помочь найти источник моей проблемы!