Получение "пропущенного FROM-предложения" Ошибка программирования в запросе django - PullRequest
2 голосов
/ 05 апреля 2019

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

Я бы хотел исключить уже подписанные подписки и подписки, для которых доступна более новая подписка.

Запрос следующий:

Subscription.objects.filter(
    end_date__gte=timezone.now(),
    end_date__lte=timezone.now() + timedelta(days=14),
).exclude(
    Q(notifications__type=Notification.AUTORENEWAL_IN_14) | Q(device__subscriptions__start_date__gt=F('start_date'))
)

Без части | Q(device__subscriptions__start_date__gt=F('start_date') запрос работает отлично. При этом django (postgres) вызывает следующую ошибку:

django.db.utils.ProgrammingError: missing FROM-clause entry for table "u1"
LINE 1: ...ption" U0 INNER JOIN "orders_subscription" U2 ON (U1."id" = ...

Я проверил sql, и он кажется неправильным:

SELECT "orders_subscription"."id",
       "orders_subscription"."months",
       "orders_subscription"."end_date",
       "orders_subscription"."start_date",
       "orders_subscription"."order_id",
       "orders_subscription"."device_id",
FROM "orders_subscription"
WHERE ("orders_subscription"."churned" = false
       AND "orders_subscription"."end_date" >= '2019-04-05T13:27:39.808393+00:00'::timestamptz
       AND "orders_subscription"."end_date" <= '2019-04-19T13:27:39.808412+00:00'::timestamptz
       AND NOT (("orders_subscription"."id" IN
                   (SELECT U1."subscription_id"
                    FROM "notifications_notification" U1
                    WHERE (U1."type" = 'AUTORENEWAL_IN_2W'
                           AND U1."subscription_id" IS NOT NULL))
                 OR ("orders_subscription"."device_id" IN
                       (SELECT U2."device_id"
                        FROM "orders_subscription" U0
                        INNER JOIN "orders_subscription" U2 ON (U1."id" = U2."device_id")
                        WHERE (U2."start_date" > (U0."start_date")
                               AND U2."device_id" IS NOT NULL))
                     AND "orders_subscription"."device_id" IS NOT NULL)))) LIMIT 21

Execution time: 0.030680s [Database: default]

Это та часть, которая вызывает проблему:

INNER JOIN "orders_subscription" U2 ON (U1."id" = U2."device_id")
 WHERE (U2."start_date" > (U0."start_date")
                               AND U2."device_id" IS NOT NULL))

U1 нигде не определен (это локально в другом предложении, но это не имеет значения.

Реляционная модель довольно проста: устройство может иметь много подписок, подписка может иметь много (разных) уведомлений.

class Subscription(models.Model):
    end_date = models.DateTimeField(null=True, blank=True)
    start_date = models.DateTimeField(null=True, blank=True)

    device = models.ForeignKey(Device, on_delete=models.SET_NULL, null=True, blank=True, related_name="subscriptions")
    # Other non significatn fields

class Device(models.Model):
    # No relational fields

class Notification(models.Model):
    subscription = models.ForeignKey('orders.Subscription', related_name="notifications", null=True, blank=True, on_delete=models.SET_NULL)
    # Other non significatn fields

Итак, мой вопрос: мой запрос неправильный или это ошибка в генераторе запросов Django ORM?

1 Ответ

2 голосов
/ 06 апреля 2019

ORM явно не может перевести ваше предложение в SQL. Это происходит даже тогда, когда предложение используется изолированно (без предыдущего предложения, то есть без псевдонима U1 в любом месте запроса).

Помимо несуществующего псевдонима, ORM также неправильно идентифицирует происхождение F('start_date') - это основной orders_subscription (тот, у которого нет псевдонима), а не любая таблица с псевдонимами из подвыбора.

Вы можете помочь ORM, определив правильный подзапрос самостоятельно.

(Попытки, приведенные ниже, основаны на предположении, что цель этого пункта состоит в том, чтобы исключить подписки, которые имеют родственные подписки (= то же родительское устройство) с более поздними датами.)

Итак, вот фильтр исключения с исправленным предложением:

from django.db.models import OuterRef, Subquery

qs.exclude(
    Q(notifications__type=Notification.AUTORENEWAL_IN_14) |
    Q(device_id__in=Subquery(Subscription.objects
        .filter(
            device_id=OuterRef('device_id'), 
            start_date__gt=OuterRef('start_date'))
        .values('device_id')
    ))
)

Однако, если присмотреться к фильтру, мы выбираем столбец (device_id), значение которого мы только что передали в качестве условия фильтра. Это лучше выражается в подзапросе Exists :

from django.db.models import OuterRef, Exists

(qs.annotate(has_younger_siblings=Exists(Subscription.objects.filter(
            device_id=OuterRef('device_id'), 
            start_date__gt=OuterRef('start_date'))))
  .exclude(has_younger_siblings=True)
  .exclude(notifications__type=Notification.AUTORENEWAL_IN_14)
)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...