Django: Как "объединить" два набора запросов с помощью Prefetch Object?

Context

Я совсем новичок в 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
    )

Query

Я пытаюсь достичь следующего. Для данного Result я хочу получить все ResultSignal, принадлежащие ему, отфильтровать их, чтобы сохранить интересующие меня, и аннотировать их двумя полями, которые мы назовем filter_group_id и filter_group_name. Значения двух полей определяются SignalValue данного ResultSignal.

С моей точки зрения, самый простой способ достичь этого - сначала аннотировать SignalValue с соответствующими filter_group_name и filter_group_id, а затем соединить получившиеся QuerySet с ResultSignal. Однако, я думаю, что в Django невозможно соединить два QuerySet вместе. Следовательно, я подумал, что мы могли бы использовать объекты Prefetch для достижения того, что я пытаюсь сделать, но похоже, что я не могу заставить это работать должным образом.

Code

Теперь я опишу текущее состояние моих запросов.

Сначала аннотируем SignalValue с соответствующими filter_group_name и filter_group_id. Обратите внимание, что filter_aggregator в следующем коде - это просто сложный фильтр, который позволяет мне выбрать только нужные SignalValue. group_filter - это тот же фильтр, но в виде списка подфильтров. Кроме того, filter_name_case - это условное выражение (конструкция Case()):

# 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
)

Затем, пытаясь присоединиться/аннотировать SignalResults:

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 должно быть поле "test", которое сейчас доступно, которое будет содержать поля signal_filter, первого QuerySet. Однако Django жалуется, что "test" не найден при вызове .values(...) в последней части моего кода: Cannot resolve keyword 'test' into field. Choices are: [...]. Как будто параметр to_attr объекта Prefetch вообще не был принят во внимание.

Вопросы

  1. Did I missunderstand the functioning of annotate() and prefetch_related() functions? If not, what am I doing wrong in my code for the specified parameter to_attr to not exist in my resulting QuerySet?
  2. Is there a better way to join two QuerySets in Django or am I better off using RawSQL? An alternative way would be to switch to Pandas to make the join in-memory, but it is very often more efficient to do such transformations on the SQL side with well-designed queries.

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

  1. Ваши аннотации верны, но "тестовая" предварительная выборка на самом деле не является атрибутом. Вы пакетно обрабатываете запросы SELECT * FROM signal_value, чтобы не выполнять select для каждой строки. Просто отбросьте аннотацию "test", и все будет в порядке. https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related

  2. Пожалуйста, не используйте pandas, это определенно не нужно и является тонной накладных расходов. Как вы сами говорите, эффективнее выполнять преобразования на стороне sql

    .

Из документации по prefetch_related:

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

Не очевидно, но вызов values() является частью этих цепочек методов, которые подразумевают другой запрос, и на самом деле отменяет prefetch_related. Это должно работать, если вы удалите его.

Вернуться на верх