Джанго комплексная аннотация - PullRequest
0 голосов
/ 07 октября 2018

Предварительные условия:

  • Queryset должен возвращать Article s
  • Queryset должен возвращать уникальные объекты
  • Не должен использоваться дляциклы, попадающие в базу данных (то есть N запросов на N объектов для аннотирования)

мои модели:

class Report(BaseModel):
    ios_report = JSONField()
    android_report = JSONField()

class Article(BaseModel):

    internal_id = models.IntegerField(unique=True)
    title = models.CharField(max_length=500)
    short_title = models.CharField(max_length=500)
    picture_url = models.URLField()
    published_date = models.DateField()
    clip_link = models.URLField()
    reports = models.ManyToManyField(
        "Report", through="ArticleInReport", related_name="articles"
    )

class ArticleInReport(BaseModel):

    article = models.ForeignKey("core.Article", on_delete=models.CASCADE, related_name='articleinreports')
    report = models.ForeignKey("core.Report", on_delete=models.CASCADE, related_name='articleinreports')
    ios_views = models.IntegerField()
    android_views = models.IntegerField()


    @property
    def total_views(self):
        return self.ios_views + self.android_views

Все начинается с объекта Reportэто создается через установленные интервалы.Этот отчет содержит данные о статьях и их соответствующих просмотров.Report будет иметь отношение с Article до ArticleInReport, которое содержит общее число пользователей в Article на момент импорта отчета .

ВНа мой взгляд, мне нужно отобразить следующую информацию:

  • Все статьи, которые получили просмотры за последние 30 минут.
  • Каждая статья помечена следующей информацией, и это гдеЯ сталкиваюсь с проблемой:

Если присутствует, количество просмотров объекта Article в last Report.Если нет, то 0.

мой views.py файл:

reports_in_time_range = Report.objects.filter(created_date__range=[starting_range, right_now]).order_by('created_date')

last_report = reports_in_time_range.prefetch_related('articles').last()
unique_articles = Article.objects.filter(articleinreports__report__in=reports_in_time_range).distinct('id')

articles = Article.objects.filter(id__in=unique_articles).distinct('id').annotate(
    total_views=Case(
            When(id__in=last_report.articles.values_list('id', flat=True),
                 then=F('articleinreports__ios_views') + F('articleinreports__android_views')),
            default=0, output_field=IntegerField(),
    ))

Некоторые объяснения моего мыслительного процесса: во-первых, получите мне только статьи, которыепоявляются в соответствующих отчетах за промежуток времени (filter(id__in=unique_articles)), возвращают только отдельные статьи.Затем, если идентификатор статьи появляется в списке статей последний отчет (конечно, через ArticleInReport), рассчитайте количество просмотров iOS + Android для этого ArticleInReport.

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

Ответы [ 3 ]

0 голосов
/ 08 октября 2018

Я вижу проблему с then=F('articleinreports__ios_views') + F('articleinreports__android_views'), потому что он не знает, какой ArticleInReport использовать .... Поэтому он, вероятно, создаст дубликаты для каждого ArticleInReport, связанного с каждой статьей.Как подсказывает @daniherrera, вы можете сначала получить все необходимые вам статьи, а затем извлечь весь ArticleInReport из последнего отчета, это будет 3 запроса.Затем вы можете просто просмотреть статьи и, если у вас есть ArticleInReport для статьи, назначить количество просмотров, если нет - присвоить ноль.Это будет работать, если вам не нужны дальнейшие SQL-операции с total_views.Возможно, вы захотите создать словарь {Article.id: ArticleInReport} перед циклом для облегчения поиска.

Другой подход (если вам нужна фильтрация или сортировка или что-то еще) заключается в использовании Subquery ArticleInReport из последнего отчета для добавления аннотации total_views для набора запросов Article.После этого вы можете использовать оператор Coalesce, чтобы заменить ноль на ноль, если статья не получила просмотров в последнем отчете.

PS Я думаю, что prefetch_related('articles') бесполезен, потому что вы все равно используете values_list.PP S также вам не нужны отличные для unique_articles и статей, потому что __in поиск уже даст отличный результат

0 голосов
/ 08 октября 2018

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

reports_in_time_range = ( Report
                         .objects
                         .filter(created_date__range=[starting_range, right_now])
                         .order_by('created_date'))

last_report = reports_in_time_range.prefetch_related('articles').last()

report_articles_ids = ( Article
                       .objects
                       .filter(articleinreports__report=last_report)
                       .values_list('id', flat=True)
                       .distinct())

report_articles = ( Article
                   .objects
                   .filter(id__in=report_articles_ids)
                   .annotate( total_views=Sum(  
                                   F('articleinreports__ios_views') +
                                   F('articleinreports__android_views'),
                                   output_field=IntegerField()
                   )))

other_articles = ( Article
                   .objects
                   .exclude(id__in=report_articles_ids)
                   .annotate( total_views=ExpressionWrapper(
                                    Value(0),
                                    output_field=IntegerField())
                   )))

articles = report_articles | other_articles
0 голосов
/ 08 октября 2018

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

articles_with_views_in_range = (
    Article.objects
        .annotate(
              total_views=Case(
                  When(articleinreports__range=(start_range, end_range), 
                       then=F('articleinreports__ios_views') + F('articleinreports__android_views')),
                  default=0, output_field=IntegerField(),
              )
        ).filter(total_views__gt=0)
  )
...