Django ORM Query, чтобы получить количество пользователей, которые в настоящее время подключены к подписке

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

Моя цель - один раз попасть в базу данных (sqlite), чтобы получить количество подписанных пользователей, мне не нужны объекты, просто число.

модели.py

class MailingListEvent(models.Model):
    """Events on mailing lists.

    This represents subscribes, unsubscribes, and bounces.  We'd like
    to understand what happens and when, not just the current state of
    the system.

    """
    class EventType(models.TextChoices):
        SUBSCRIBE = 'sub', 'inscription'
        UNSUBSCRIBE = 'unsub', 'désinscription'
        BOUNCE = 'bounce', 'bounce'

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    mailing_list = models.ForeignKey(MailingList,
                                     on_delete=models.CASCADE)
    event_timestamp = models.DateTimeField(default=django.utils.timezone.now)
    event_type = models.CharField(max_length=6, choices=EventType.choices)

Пока я нашел только это решение для работы:

def user_subscribe_count(mailing_list):
    """Return the number of users currently subscribed to the mailing list.

    We want to know how many users are currently subscribed.  Note
    that individual users might subscribe and unsubscribe multiple
    times.  Other (future) events could happen as well.
    """
    user_list = MailingListEvent.objects.filter(mailing_list=mailing_list).values_list('user',flat=True).distinct()
    users_subscribed = list()
    for user in user_list:
        user_state = user_current_state(User.objects.get(pk=user),mailing_list)
        if user_state.event_type == MailingListEvent.EventType.SUBSCRIBE:
           users_subscribed.append(user_state)
    return len(users_subscribed)

def user_current_state(user, mailing_list):
    """Return user's most current state on the provided mailing list

    Return the most recent event associated with this user in this
    mailing list.

    """
    try:
        the_user = MailingListEvent.objects.filter(
            Q(event_type=MailingListEvent.EventType.SUBSCRIBE) |
            Q(event_type=MailingListEvent.EventType.UNSUBSCRIBE),
            user=user, mailing_list=mailing_list).latest(
                'event_timestamp')
        return the_user
    except MailingListEvent.DoesNotExist:
        return MailingListEvent(
            user=user, mailing_list=mailing_list,
            event_type=MailingListEvent.EventType.UNSUBSCRIBE)

Я знаю, что в Django есть метод .count(), но преобразование в список все равно уже попадает в базу данных.

Может ли кто-нибудь предложить запрос, который вернет количество пользователей, подписанных в настоящее время, с учетом этой модели, пожалуйста?

Итак, это подсчет количества событий подписки минус количество событий отказа от подписки?

Willem Van Onsem 06.04.2022 10:23

@WillemVanOnsem Не совсем так, один пользователь теоретически может сделать несколько событий отмены подписки через предоставленную нами ссылку для отмены подписки. Я думаю, мы могли бы ограничить это поведение, а затем выполнить SUB - UNSUB, чтобы получить правильное число.

Benjamin_Mourgues 06.04.2022 10:34

Можете показать реализацию user_current_state?

Willem Van Onsem 06.04.2022 11:50

@WillemVanOnsem Добавил реализацию в тему.

Benjamin_Mourgues 06.04.2022 16:07
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
49
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете аннотировать User последними MailingListEvent для этого MailingList, используя Выражение Subquery [Django-doc]:

from django.db.models import OuterRef, Subquery

User.objects.alias(
    latest_action=Subquery(
        MailingListEvent.objects.filter(
            mailing_list=mailing_list,
            user=OuterRef('pk')
        ).order_by('-event_timestamp').values('event_type')[:1]
    )
).filter(latest_action='sub').count()

Если последним действием было BOUNCE, то это нет будет считаться подписанным пользователем.


Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.


Note: Django's DateTimeField [Django-doc] has a auto_now_add=… parameter [Django-doc] to work with timestamps. This will automatically assign the current datetime when creating the object, and mark it as non-editable (editable=False), such that it does not appear in ModelForms by default.

Спасибо за предложение! Я реализовал это и протестировал решение в случае, когда вывод должен был быть 2 (один пользователь просто подписался, другой сделал подписку-отмену-под-отмену-подписку), к сожалению, он вернул 0.

Benjamin_Mourgues 06.04.2022 16:40

@Benjamin_Mourgues: что вы получаете, когда используете .values('latest_action') вместо .filter(latest_action='sub').count(), обычно это должно указывать последнее действие на User.

Willem Van Onsem 06.04.2022 16:43

Я получаю FieldError: Cannot select the 'latest_action' alias. Use annotate() to promote it.

Benjamin_Mourgues 06.04.2022 16:47

@Benjamin_Mourgues: и в этом случае используйте .annotate(..) вместо .alias(..).

Willem Van Onsem 06.04.2022 16:47

Поэтому я отредактировал предложенную вами функцию с помощью annotate вместо alias, она по-прежнему возвращает 0. Однако я применил .values('latest_action'), чтобы увидеть, какие выходные данные она производит, и это длинный набор запросов, полный {'latest_action': None}. Спасибо за помощь кстати, я ценю.

Benjamin_Mourgues 06.04.2022 16:55

@Benjamin_Mourgues: вы уверены, что используете правильный mailing_list? Похоже, что, возможно, для другого списка рассылки пользователи подписаны/отписаны?

Willem Van Onsem 06.04.2022 16:58

Другие вопросы по теме