Django queryset: аннотация с вычисляемым значением

Я делаю очень простую систему уведомлений для своего сайта, работающую на основе API Django REST Framework. Она предназначена для отправки обновлений сайта и прочего всем пользователям, все получают одинаковые уведомления, и они могут пометить их как прочитанные/заархивировать. Я придумал следующую модель:

class Notification(models.Model):
    title = models.CharField(max_length=255)
    text = models.TextField()
    type = models.CharField(max_length=255, blank=True)
    read_by = models.ManyToManyField(User, blank=True, related_name="read_notifications")
    archived_by = models.ManyToManyField(User, blank=True, related_name="archived_notifications")
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)

Поэтому нет поля получателя или чего-то подобного, так как все пользователи все равно получают все уведомления.

Теперь я пытаюсь написать логику представления, в частности, следующие 2 вещи: получать только неархивированные уведомления, сделанные после создания пользователя, и добавить вычисляемое поле "is_read" к нему, таким образом, чтобы не делать дополнительные запросы для каждого отдельного уведомления / комбинации пользователя.

Запрос выглядит следующим образом:

queryset = Notification.objects
    .order_by("-created_at")
    .filter(created_at__gt=self.request.user.created_at)
    .exclude(archived_by=self.request.user)

Это действительно отфильтровывает архивные запросы, как и ожидалось, и я думаю, что он делает это без дополнительного запроса для каждого уведомления:

SELECT "notifications_notification"."id", "notifications_notification"."title", "notifications_notification"."text", "notifications_notification"."type", "notifications_notification"."created_at", "notifications_notification"."updated_at" FROM "notifications_notification" WHERE ("notifications_notification"."created_at" > 2022-09-26 12:44:04.771961+00:00 AND NOT (EXISTS(SELECT 1 AS "a" FROM "notifications_notification_archived_by" U1 WHERE (U1."user_id" = 1 AND U1."notification_id" = ("notifications_notification"."id")) LIMIT 1))) ORDER BY "notifications_notification"."created_at" DESC

Пока все хорошо! Но мне все еще нужно как-то добавить значение "is_read" (или "is_unread", если проще) в запрос, что я никак не могу придумать, как сделать.

Как я могу завершить запрос и сделать его более производительным?

После проб и ошибок я пришел к следующему:

queryset = Notification.objects.order_by("-created_at")
    .filter(created_at__gt=self.request.user.created_at)
    .exclude(archived_by=self.request.user)
    .annotate(is_read=Exists(Notification.objects.filter(pk=OuterRef("id"), read_by=self.request.user)))

И это работает, хотя делает 2 подзапроса, и мне интересно, не станет ли это узким местом в дальнейшем?

SELECT "notifications_notification"."id", "notifications_notification"."title", "notifications_notification"."text", "notifications_notification"."type", "notifications_notification"."created_at", "notifications_notification"."updated_at", EXISTS(SELECT 1 AS "a" FROM "notifications_notification" U0 INNER JOIN "notifications_notification_read_by" U1 ON (U0."id" = U1."notification_id") WHERE (U0."id" = ("notifications_notification"."id") AND U1."user_id" = 1) LIMIT 1) AS "is_read" FROM "notifications_notification" WHERE ("notifications_notification"."created_at" > 2022-09-26 14:40:29.368043+00:00 AND NOT (EXISTS(SELECT 1 AS "a" FROM "notifications_notification_archived_by" U1 WHERE (U1."user_id" = 1 AND U1."notification_id" = ("notifications_notification"."id")) LIMIT 1))) ORDER BY "notifications_notification"."created_at" DESC

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