Django REST框架按字段分组并添加额外内容

Aam*_*amu 13 django django-rest-framework

我有票预订模型

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)
Run Code Online (Sandbox Code Playgroud)

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

[
    {
        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
    }
]
Run Code Online (Sandbox Code Playgroud)

我试过这种方式:

>>> 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}]>
Run Code Online (Sandbox Code Playgroud)

但它没有根据节目分组.此外,我怎么可以添加其他相关领域(即show__movie__name,show__day__date,show__time)

hyn*_*cer 5

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

          +-------------------------+
          | MovieTicket (booked_at) |
          +-----+--------------+----+
                |              |
      +---------+--------+  +--+---+
      |    Show (time)   |  | User |
      ++----------------++  +------+
       |                |
+------+-------+  +-----+------+
| Movie (name) |  | Day (date) |
+--------------+  +------------+
Run Code Online (Sandbox Code Playgroud)

问题是:如何汇总按Show(一个相关对象),由User(另一个相关对象)过滤的Show(一个相关对象)分组的MovieTicket(最顶层的对象),以及来自一些相关的更深层对象(Movie和Day)的报告详细信息,以及如何通过汇总的某些字段对这些结果进行排序按组中最高的模型(按组中最近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')
      )
Run Code Online (Sandbox Code Playgroud)

可以将zaphod100.10在他的答案中演示的查询集轻松地转换为JSON,或者直接用这种方式对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
]))
Run Code Online (Sandbox Code Playgroud)

验证查询:

          +-------------------------+
          | MovieTicket (booked_at) |
          +-----+--------------+----+
                |              |
      +---------+--------+  +--+---+
      |    Show (time)   |  | User |
      ++----------------++  +------+
       |                |
+------+-------+  +-----+------+
| Movie (name) |  | Day (date) |
+--------------+  +------------+
Run Code Online (Sandbox Code Playgroud)
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')
      )
Run Code Online (Sandbox Code Playgroud)

笔记:

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

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

  • 相关的模型Day不是直观​​的,但是很显然,它具有一个字段,date并且希望还有一些非琐碎的字段,这对于安排诸如电影节假期之类的事件的放映可能很重要。将字段“日期”设置为Day模型的主键并在诸如此类的许多查询中经常避免进行关系查找会很有用。

(这个答案的所有重要部件能够在最古老的两个答案中找到:托多尔和zaphod100.10不幸的是,这些答案都没有被组合在一起,然后不起来,通过投票的人除了我,甚至认为这个问题有很多了赞成票。 )


zap*_*.10 2

您必须按演出进行分组,然后计算电影票的总数。

MovieTicket.objects.filter(user=23).values('show').annotate(total_tickets=Count('show')).values('show', 'total_tickets', 'show__movie__name', 'show__time', 'show__day__date'))
Run Code Online (Sandbox Code Playgroud)

将此序列化器类用于上述查询集。它将给出所需的 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()
Run Code Online (Sandbox Code Playgroud)

不可能按 Booking_at 排序,因为当我们按演出分组时该信息会丢失。如果我们通过 booking_at 进行订购,则 group by 将在唯一的 booking_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')))
Run Code Online (Sandbox Code Playgroud)

您不能在带注释的字段上进行注释。所以你会在 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']
Run Code Online (Sandbox Code Playgroud)

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

上面相同的序列化器可以与这些对象一起使用。