Django:使用传播数据优化查询

时间:2019-02-22 10:55:19

标签: python django django-models django-queryset

我有Order个对象和OrderOperation个对象,它们表示对订单的操作(创建,修改,取消)。

从概念上讲,订单具有1到许多订单操作。每次对订单执行一次操作时,都会在此操作中计算总数。这意味着当我需要查找订单总数时,我只会得到最后的订单操作总数。

简化代码

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)

class Order(models.Model):

    @property
    def last_operation(self) -> Optional['OrderOperation']:
        try:
            qs = self.orderoperation_set.all()
            return qs[len(qs) - 1]
        except AssertionError:  # when there is a negative indexing (no operation)
            # IndexError can not happen
            return None

    @property
    def total(self) -> Optional[Decimal]:
        last_operation = self.last_operation
        return last_operation.total if last_operation else None

问题

由于我收到很多订单,因此每次我要进行简单的过滤(例如“总价低于5欧元的订单”)时,都需要很长时间,因为我需要使用以下内容浏览所有订单,显然是错误的查询:

all_objects = Order.objects.all()
Order.objects.prefetch_related('orderoperation_set').filter(
    pk__in=[o.pk for o in all_objects if o.total <= some_value])

我当前的想法/我尝试过的事情

数据非规范化?

我可以简单地在total上创建一个Order属性,并将每次操作创建时将操作总数复制到订单总数中。 然后,Order.objects.filter(total__lte=some_value)将起作用。 但是,在复制数据库中的数据之前,我想确保没有更简单/更干净的解决方案。

使用anateate()方法?

我希望能够做到:Order.objects.annotate(total=something_magical_here).filter(total__lte=some_value)。似乎不可能。

先过滤然后匹配?

order_operations = OrderOperation.objects.filter(total__lte=some_value)
orders = Order.objects.filter(orderoperation__in=order_operations)

这非常快,但是由于我没有过滤最后的操作,但是所有的操作都在这里,因此过滤效果很差。这是错误的。

还有其他想法吗?谢谢。

1 个答案:

答案 0 :(得分:1)

使用annotate()方法

  

似乎不可能。

当然,这是可能的;)您可以使用子查询或一些巧妙的条件表达式。假设您想从上次订单操作中获得总金额,下面是带有子查询的示例:

from django.db.models import Subquery, OuterRef

orders = Order.objects.annotate(
    total=Subquery(                             # [1]
        OrderOperation.objects \
            .filter(order_id=OuterRef("pk")) \  # [2]
            .order_by('-id') \                  # [3]
            .values('total') \                  # [4]
            [:1]                                # [5]
    )
)

上面的代码说明:

  1. 我们正在向结果列表添加新字段,称为total的字段将由子查询填充。您可以在此查询集中将其作为模型Order的任何其他字段进行访问(在评估之后,在模型实例中或在过滤和其他注释中)。您可以从Django docs了解注释的工作原理。
  2. 子查询仅应用于当前顺序的操作。 OuterRef只会替换为对结果SQL查询中所选字段的引用。
  3. 我们要按操作id降序排列,因为我们确实想要最新的。如果您要在操作中有其他字段要排序(例如创建日期),请在此处填写。
  4. 该子查询应仅从操作返回total
  5. 我们只需要一个元素。使用切片表示法而不是普通索引来获取它,因为在Django查询集上使用index会立即调用它。切片仅将LIMIT子句添加到SQL查询中,而无需调用它,这就是我们想要的。

现在您可以使用:

orders.filter(total__lte=some_value)

仅获取所需的订单。您也可以使用该注释来