【问题标题】:Django REST framework Group by fields and add extra contentsDjango REST framework 按字段分组并添加额外内容
【发布时间】:2017-04-23 17:31:05
【问题描述】:

我有一个订票模型

class Movie(models.Model):
    name = models.CharField(max_length=254, unique=True)

class Show(models.Model):
    day = models.ForeignKey(Day)
    time = models.TimeField(choices=CHOICE_TIME)
    movie = models.ForeignKey(Movie)

class MovieTicket(models.Model):
    show = models.ForeignKey(Show)
    user = models.ForeignKey(User)
    booked_at = models.DateTimeField(default=timezone.now)

我想用其user 字段过滤MovieTicket,并根据其show 字段对它们进行分组,并按最近预订的时间对其进行排序。并使用 Django REST 框架回复json 数据,如下所示:

[
    {
        show: 4,
        movie: "Lion king",
        time: "07:00 pm",
        day: "23 Apr 2017",
        total_tickets = 2
    },
    {
        show: 7,
        movie: "Gone girl",
        time: "02:30 pm",
        day: "23 Apr 2017",
        total_tickets = 1
    }
]

我试过这样:

>>> MovieTicket.objects.filter(user=23).order_by('-booked_at').values('show').annotate(total_tickets=Count('show'))
<QuerySet [{'total_tickets': 1, 'show': 4}, {'total_tickets': 1, 'show': 4}, {'total_tickets': 1, 'show': 7}]>

但它没有按节目分组。另外如何添加其他相关字段(即show__movie__nameshow__day__dateshow__time

【问题讨论】:

    标签: django django-rest-framework


    【解决方案1】:

    我在数据库模型的图表上更一般地解释它。它可以应用于任何具有额外内容的“GROUP BY”。

              +-------------------------+
              | MovieTicket (booked_at) |
              +-----+--------------+----+
                    |              |
          +---------+--------+  +--+---+
          |    Show (time)   |  | User |
          ++----------------++  +------+
           |                |
    +------+-------+  +-----+------+
    | Movie (name) |  | Day (date) |
    +--------------+  +------------+
    

    问题是:如何总结由用户(其他相关对象)过滤的 Show(一个相关对象)分组的 MovieTicket(最顶层对象)以及来自一些相关更深层对象(电影和天)并按组从最顶层模型聚合的某个字段对这些结果进行排序(按组中最近 MovieTicket 的预订时间):

    通过更一般的步骤解释答案:

    • 从最上面的模型开始:
      (MovieTicket.objects ...)
    • 应用过滤器:
      .filter(user=user)
    • pk 对最近的相关模型进行分组很重要(至少是那些未被过滤器保持不变的模型) - 它只是“显示”(因为“用户”对象仍被过滤到一个用户)
      .values('show_id')
      即使所有其他字段一起是唯一的(show__movie__name、show__day__date、show__time),数据库引擎优化器最好按 show_id 对查询进行分组,因为所有这些其他字段都依赖于 show_id 并且不会影响组的数量。
    • 注释必要的聚合函数:
      .annotate(total_tickets=Count('show'), last_booking=Max('booked_at'))
    • 添加必需的依赖字段:
      .values('show_id', 'show__movie__name', 'show__day__date', 'show__time')
    • 按需要排序:
      .order_by('-last_booking')(从最新到最旧降序)
      在没有通过聚合函数封装的情况下,不输出或排序最顶层模型的任何字段是非常重要的。 (MinMax 函数非常适合从组中采样。未通过聚合封装的每个字段都将添加到“分组依据”列表中,这将打破预期的组。朋友的同一个节目的更多门票可能是逐步预订,但应一起计算并按最新预订报告。)

    放在一起:

    from django.db.models import Max
    
    qs = (MovieTicket.objects
          .filter(user=user)
          .values('show_id', 'show__movie__name', 'show__day__date', 'show__time')
          .annotate(total_tickets=Count('show'), last_booking=Max('booked_at'))
          .order_by('-last_booking')
          )
    

    查询集可以很容易地转换为 JSON 如何在他的回答中演示 zaphod100.10,或者直接以这种方式为对 django-rest 框架不感兴趣的人展示:

    from collections import OrderedDict
    import json
    
    print(json.dumps([
        OrderedDict(
            ('show', x['show_id']),
            ('movie', x['show__movie__name']),
            ('time', x['show__time']),      # add time formatting
            ('day': x['show__day__date']),  # add date formatting
            ('total_tickets', x['total_tickets']),
            # field 'last_booking' is unused
        ) for x in qs
    ]))
    

    验证查询:

    >>> print(str(qs.query))
    
    SELECT app_movieticket.show_id, app_movie.name, app_day.date, app_show.time,
        COUNT(app_movieticket.show_id) AS total_tickets,
        MAX(app_movieticket.booked_at) AS last_booking
    FROM app_movieticket
    INNER JOIN app_show ON (app_movieticket.show_id = app_show.id)
    INNER JOIN app_movie ON (app_show.movie_id = app_movie.id)
    INNER JOIN app_day ON (app_show.day_id = app_day.id)
    WHERE app_movieticket.user_id = 23
    GROUP BY app_movieticket.show_id, app_movie.name, app_day.date, app_show.time
    ORDER BY last_booking DESC
    

    注意事项:

    • 模型图类似于 ManyToMany 关系,但 MovieTicket 是单独的对象,可能包含座位号。

    • 一个查询很容易为更多用户获得类似的报告。字段“user_id”和名称将添加到“values(...)”中。

    • 相关模型 Day 并不直观,但很明显它有一个字段 date 并且希望还有一些非平凡的字段,对于安排与电影假期等事件相关的节目可能很重要。将字段“日期”设置为 Day 模型的主键并在许多类似这样的查询中频繁地进行关系查找会很有用。

    (此答案的所有重要部分都可以在最旧的两个答案中找到:Todor 和 zaphod100.10。不幸的是,这些答案没有组合在一起,然后除了我之外的任何人都没有投票,即使这个问题有很多赞成票。)

    【讨论】:

    • 在这个答案中,您是使用 drf 序列化程序还是自己构建 json?
    • @HarryMoreno:这个问题和答案的重要部分是如何编写正确的查询集。序列化很容易,两种选择都是可能的。
    • 不知道如何序列化信息。对不起。
    【解决方案2】:

    我想使用用户字段过滤 MovieTicket 并将它们分组 根据其显示字段,并按最近预订时间排序。

    这个queryset 会给你你想要的:

    tickets = (MovieTicket.objects
                .filter(user=request.user)
                .values('show')
                .annotate(last_booking=Max('booked_at'))
                .order_by('-last_booking')
    )
    

    然后使用 Django rest 框架返回 json 数据,如下所示: [ { 显示:4, 电影:《狮子王》, 时间:“07:00 pm”, 日期:“2017 年 4 月 23 日”, 总票数 = 2 }, { 显示:7, 电影:《消失的女孩》, 时间:“02:30 pm”, 日期:“2017 年 4 月 23 日”, 总票数 = 1 } ]

    这个json数据和你描述的查询不一样。您可以通过将注解和show__movie__name 扩展至.values 子句来添加total_tickets:这会将分组更改为show+movie_name,但由于show 只有一个movie_name,所以没关系。

    但是,您不能添加show__day__dateshow__time,因为一个节目有多个日期时间,那么您希望组中的哪一个?例如,您可以获取最大的daytime,但这并不能保证您在这一天+时间会有一个节目,因为这些是不同的字段,彼此不相关。所以最后的尝试可能看起来像:

    tickets = (MovieTicket.objects
                .filter(user=request.user)
                .values('show', 'show__movie__name')
                .annotate(
                    last_booking=Max('booked_at'),
                    total_tickets=Count('pk'),
                    last_day=Max('show__day'),
                    last_time=Max('show__time'),
                )
                .order_by('-last_booking')
    )
    

    【讨论】:

    • 我自己使用了你的答案。没有必要在last_time=Max('show__time')last_day=Max('show__day') 调用聚合,而'show__day__date' 应该是由于奇怪的模型。否则,这是一个完美的答案,用annotate 重命名字段是个好主意,即.annotate(..., day=Value('show__day__date'), time=Value('show__time'))
    • MySQL 是唯一的 RDBMSallow you 执行此操作。通常禁止在 group by 子句之外的 select 子句中包含非聚合值。我看了你的回答,我认为这个查询会返回不正确的结果,因为你在 group by 子句中有datetime。正确的方法是.annotate(last_show_time=Max(DateTime(date='show__date', time='show__time'))) 当然这不是开箱即用的,但我认为它不是不可能做到的。
    • 我错过了正确的名称models.F(),否则它会按预期工作。它 如果查询是aggregation/group_by 查询,则将没有聚合功能的每个字段或注解都添加到“group by”中。例如查询集... .annotate(movie=F('show__movie__name'))...的这个添加部分是通过添加这些SQL部分SELECT ..., app_movie.name AS movie, ... GROUP BY ... app_movie.name, ...来编译的。
    • 是的,ORM 正在添加它,因为它禁止不存在,但一旦添加 show__day__dateshow__time 更改分组导致不正确的结果,即你不想要将这些字段放入 group by 子句中。
    • OP 希望按他们分组。字段“show”:show_id 是他的 JSON 的一部分。 Show的所有字段都依赖于show_id。如果将任何依赖字段添加到超出 show_id 的 group by,则不会细化 Group By 结果。同样是递归添加其相关对象的字段(电影,日)。唯一需要对每个使用的字段进行聚合的对象是最顶层的对象(MovieTicket - pk,booked_at)...
    【解决方案3】:

    你可以试试这个。

    Show.objects.filter(movieticket_sets__user=23).values('id').annotate(total_tickets=Count('movieticket_set__user')).values('movie__name', 'time', 'day').distinct()
    

    Show.objects.filter(movieticket_sets__user=23).values('id').annotate(total_tickets=Count('id')).values('movie__name', 'time', 'day').distinct()
    

    【讨论】:

    • 无效。通过将所有movieticket_sets__user 替换为movieticket__user 来修复它。
    【解决方案4】:

    你必须按节目分组,然后计算电影票的总数。

    MovieTicket.objects.filter(user=23).values('show').annotate(total_tickets=Count('show')).values('show', 'total_tickets', 'show__movie__name', 'show__time', 'show__day__date'))
    

    对上述查询集使用这个序列化器类。它将提供所需的 json 输出。

    class MySerializer(serializers.Serializer):
        show = serailizer.IntegerField()
        movie = serializer.StringField(source='show__movie__name')
        time = serializer.TimeField(source='show__time')
        day = serializer.DateField(source='show__day__date')
        total_tickets = serializer.IntegerField()
    

    不可能 order_by booking_at 因为当我们按节目分组时该信息会丢失。如果我们按booked_at 订购,group by 将在唯一的booked_at 时间发生并显示id,这就是票数为1 的原因。没有order_by,您将获得正确的计数。

    编辑:

    使用这个查询:

    queryset = (MovieTicket.objects.filter(user=23)
                .order_by('booked_at').values('show')
                .annotate(total_tickets=Count('show'))
                .values('show', 'total_tickets', 'show__movie__name',
                        'show__time', 'show__day__date')))
    

    您不能在带注释的字段上进行注释。因此,您将在 python 中找到总票数。要计算唯一节目 ID 的 total_tickets 计数:

    tickets = {}
    for obj in queryset:
        if obj['show'] not in tickets.keys():
            tickets[obj['show']] = obj
        else:
            tickets[obj['show']]['total_tickets'] += obj['total_tickets']
    

    您需要的最终对象列表是tickets.values()

    上述相同的序列化程序可以用于这些对象。

    【讨论】:

    • 我认为您的查询和我的查询大致相同,这仍然给我相同的结果(即&lt;QuerySet [{'total_tickets': 1, 'show': 4}, {'total_tickets': 1, 'show': 4}, {'total_tickets': 1, 'show': 7}]&gt;)。我想我没有完全得到你的答案。能否请您详细说明。
    • 是的,但我需要列出最近预订的机票。现在列表按照演出时间排序。有没有其他办法可以这样列出来?
    • @Aamu:您不能按演出分组,也不能按预定时间订购。如果你想通过电影票模型上的booked_at字段订购,group_by将基于booked_at和show。当您也通过预定_at 订购时,我已经添加了更多信息以获取正确的计数。
    • @Aamu 你试过我的答案了吗?
    • 我自己使用了你的答案。您的问题是 .order_by('booked_at') 如果在没有聚合Max() 的情况下使用它会破坏组。在values() 中,名称“total_tickets”并不重要。否则,这是一个完美的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-08-08
    • 2021-09-03
    • 2015-01-20
    • 2013-08-26
    • 2019-07-07
    • 2015-06-19
    相关资源
    最近更新 更多