Django 复杂注解

Django complex annotation

先决条件:

我的模特:

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 将通过 ArticleInReportArticle 建立关系,它包含导入报告时 Article 中的用户总数.

在我看来,我需要显示以下信息:

If present, the number of views the Article object had in the last Report. If not present, 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))出现在相关报告中的文章,return 只给我不同的文章。接下来,如果文章的 ID 出现在 last report 的文章列表中(当然是通过 ArticleInReport),计算 iOS views + Android ArticleInReport.

的观看次数

上面的注释对大多数 Article 都有效,但对其他注释却无缘无故地失败了。我尝试了很多不同的方法,但似乎总是得到错误的结果。

您的方法存在问题,您需要使用 IN 来匹配唯一的确切 ID 将 return 比预期的范围大,您可以直接使用反向名称来过滤文章对象,也过度使用 unique

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)
  )

我可以看到 then=F('articleinreports__ios_views') + F('articleinreports__android_views') 的问题,因为它不知道要使用哪个 ArticleInReport...。因此它可能会为与每篇文章关联的每个 ArticleInReport 创建重复项。正如@daniherrera 所建议的,您可以首先获取所需的所有文章,然后从上一份报告中获取所有 ArticleInReport,这将是 3 个查询。然后你可以循环浏览文章,如果你有文章的 ArticleInReport,分配视图计数,如果没有 - 分配零。如果您不需要对 total_views 进行任何进一步的 sql 操作,这将起作用。您可能希望在循环之前构建一个 {Article.id: ArticleInReport} 的字典以便于查找。

另一种方法(如果您需要一些过滤或排序或其他)是使用上一份报告中 ArticleInReport 的 Subquery 为 Article 查询集添加 total_views 注释。然后,您可以使用 Coalesce 运算符在文章在上一个报告中没有收到任何视图时将 Null 替换为零。

P. S、我觉得prefetch_related('articles')没用,反正你用values_list。 P. P. S 你也不需要对 unique_articles 和文章进行区分,因为 __in 查找已经产生了不同的结果

避免命中数据库非常重要,但不是以这个代价。在我看来,您应该将查询拆分为两个或多个查询。拆分查询将提高可读性,也可能提高性能(有时两个简单的查询比一个复杂的查询运行得更快)请记住,您拥有 dics、理解和 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