Как преобразовать сложный SQL-запрос Postgres в кверисет Django?

У меня работает следующий SQL-запрос:

SELECT 
    mrc.token_id, 
    ARRAY_AGG(mt.name) AS tracking
FROM 
    markets_rankscurrent mrc
LEFT JOIN (
    SELECT 
        mtg.id, 
        mtg.index_id, 
        mtg.favorites_group_id, 
        mtg.name, 
        COALESCE(ufe.token_id, mte.token_id, mie.token_id) AS token_id
    FROM 
        markets_trackinggroup mtg
    LEFT JOIN users_favoritesentry ufe ON mtg.favorites_group_id = ufe.group_id
    LEFT JOIN markets_trackingentry mte ON mtg.id = mte.group_id
    LEFT JOIN markets_indexentry mie ON mtg.index_id = mie.index_id
) mt ON mrc.token_id = mt.token_id
GROUP BY 
    mrc.token_id;

Вот мои модели:

class Token(models.Model):

class RanksCurrent(models.Model):
    token = models.ForeignKey(Token, on_delete=models.CASCADE, db_index=False)

class TrackingGroup(models.Model):
    name = models.CharField(max_length=60, verbose_name='Name')
    favorites_group = models.ForeignKey(FavoritesGroup, on_delete=models.CASCADE, related_name='tracking_groups', blank=True, null=True)
    index = models.ForeignKey(Token, on_delete=models.CASCADE, related_name='tracking_groups', blank=True, null=True)

class TrackingEntry(models.Model):
    group = models.ForeignKey(TrackingGroup, on_delete=models.CASCADE, related_name='tokens')
    token = models.ForeignKey(Token, on_delete=models.CASCADE, related_name='tracking_entries')

class IndexEntry(models.Model):
    index = models.ForeignKey(Token, on_delete=models.CASCADE, related_name='index_tokens')
    token = models.ForeignKey(Token, on_delete=models.CASCADE, related_name='indices')

class FavoritesGroup(models.Model):
    pass

class FavoritesEntry(models.Model):
    group = models.ForeignKey(FavoritesGroup, on_delete=models.CASCADE, related_name='favorites_entries')
    token = models.ForeignKey('markets.Token', on_delete=models.CASCADE, related_name='favorites_entries')

Django models

Внешний ключ TrackingGroup.index будет установлен только на объект Token, который также является внешним ключом в таблице IndexEntry.

Моей конечной целью является возможность запросить таблицу RanksCurrent и аннотировать столбец tracking_groups, который содержит список имен TrackingGroup, в которых Token является членом. В моих попытках использовать Subquery и ArrayAgg, чтобы попытаться сделать это, но если мой подзапрос возвращает список (как я хочу), он терпит неудачу.

Для получения списка имен TrackingGroup сработали следующие типы методов:

tracking_subquery = TrackingGroup.objects.filter(
    Q(index__index_tokens__token=OuterRef('token_id')) |
    Q(favorites_group__favorites_entries__token=OuterRef('token_id')) |
    Q(tokens__token=OuterRef('token_id'))
).values('name')

tracking_subquery = TrackingGroup.objects.filter(
    Q(favorites_group_id__in=FavoritesEntry.objects.filter(token_id=OuterRef(OuterRef('token_id'))).values('group_id')) |
    Q(id__in=TrackingEntry.objects.filter(token_id=OuterRef(OuterRef('token_id'))).values('group_id')) |
    Q(index_id__in=IndexEntry.objects.filter(token_id=OuterRef(OuterRef('token_id'))).values('index_id'))
).values('name')

Однако, когда я пытаюсь аннотировать основной запрос, он терпит неудачу:

RanksCurrent.objects.annotate(
    tracking=ArrayAgg(Subquery(tracking_subquery))
)

Я думал, что мне нужно будет переместить вызов ArrayAgg внутрь подзапроса, но это ничего не изменило:

RanksCurrent.objects.annotate(
    tracking=Subquery(
        TrackingGroup.objects.filter(
            Q(index__index_tokens__token=OuterRef('token_id')) |
            Q(favorites_group__favorites_entries__token=OuterRef('token_id')) |
            Q(tokens__token=OuterRef('token_id'))
        ).values('name').annotate(group_names=ArrayAgg('name')).values('group_names')))

Насколько я могу судить, SQL работает потому, что внутренний оператор SELECT возвращает уникальную строку для каждого token_id, а не строку для каждого TrackingGroup.

Сейчас я думаю разбить это на три отдельных запроса, каждый из которых будет направлен на одну из таблиц *Entry, а затем либо как-то объединить получившиеся списки в один и аннотировать им набор запросов, либо просто создать три отдельных аннотации. Я бы уже сделал это, если бы не заставил операцию работать в SQL, так что теперь я хочу попробовать сделать то же самое в Django, прежде чем полностью сдаться.

Мое текущее решение заключается в выполнении трех отдельных запросов и их объединении в сериализаторе:

tracking_index_groups_subquery = Subquery(
    queryset_filtered.filter(
        token__indices__index__tracking_groups__isnull=False,
        id=OuterRef('id')
    ).values('id').annotate(
        index_tracking=ArrayAgg('token__indices__index__tracking_groups__name')
    ).values('index_tracking')[:1]
)

tracking_favorites_groups_subquery = Subquery(
    queryset_filtered.filter(
        token__favorites_entries__group__tracking_groups__isnull=False,
        id=OuterRef('id')
    ).values('id').annotate(
        favorites_tracking=ArrayAgg('token__favorites_entries__group__tracking_groups__name')
    ).values('favorites_tracking')[:1]
)

tracking_groups_subquery = Subquery(
    queryset_filtered.filter(
        token__tracking_entries__group__isnull=False,
        id=OuterRef('id')
    ).values('id').annotate(
        tracking=ArrayAgg('token__tracking_entries__group__name')
    ).values('tracking')[:1]
)

queryset_filtered = queryset_filtered.annotate(
    index_tracking=tracking_index_groups_subquery,
    favorites_tracking=tracking_favorites_groups_subquery,
    tracking=tracking_groups_subquery
)


class RanksCurrentSerializer(serializers.ModelSerializer):
    tracking = serializers.SerializerMethodField('get_tracking')

    def get_tracking(self, obj):
        tracking = []
        if obj.index_tracking:
            tracking.extend(obj.index_tracking)
        if obj.favorites_tracking:
            tracking.extend(obj.favorites_tracking)
        if obj.tracking:
            tracking.extend(obj.tracking)
        return tracking
Вернуться на верх