Django 复杂过滤器和排序

Posted

技术标签:

【中文标题】Django 复杂过滤器和排序【英文标题】:Django complex filter and order 【发布时间】:2018-01-16 23:23:36 【问题描述】:

我有 4 个这样的模型

class Site(models.Model):
    name = models.CharField(max_length=200)

    def get_lowest_price(self, mm_date):
        '''This method returns lowest product price on a site at a particular date'''

class Category(models.Model):
    name = models.CharField(max_length=200)
    site = models.ForeignKey(Site)

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category)

class Price(models.Model):
    date = models.DateField()
    price = models.IntegerField()
    product = models.ForeignKey(Product)

这里每个都有很多类别,每个类别都有很多产品。现在产品价格每天都在变化,因此价格模型将保留产品价格和日期。

我的问题是我想要按价格范围筛选网站列表。此价格范围将取决于 get_lowest_price 方法,并且可以按 Min 到 Max 和 Max 到 Min 排序。我已经使用 lambda 表达式来做到这一点,但我认为这不合适

sorted(Site.objects.all(), key=lambda x: x.get_lowest_price(the_date))

我也可以通过运行循环来获得价格范围内的所有网站,但这也不是一个好主意。请帮助我的人以正确的方式进行查询。

如果您仍然需要更清楚地了解该问题,请参阅“Ishtiaque Khan”的第一条评论,他的假设是 100% 正确的。

*在这些模型中,写入频率低,读取频率高。

【问题讨论】:

你的业务逻辑不清楚。您似乎正在尝试查找按 V 排序的唯一站点列表,其中 V 是该站点中所有产品在给定日期的最低价格。如果这是业务逻辑,那么当前的方法不是最优的。如果您的日期范围在几年内,并且您的数据库是静态数据库,或者数据条目非常少,那么对每个新条目的每个日期的最低 V 进行急切评估似乎是个好主意。所以预先计算/急切的计算可能是要走的路。 【参考方案1】:

1.使用查询 如果您只想使用特定日期进行查询。方法如下:

q = Site.objects.filter(category__product__price__date=mm_date) \
        .annotate(min_price=Min('category__product__price__price')) \
        .filter(min_price__gte=min_price, min_price__lte=max_price)

它将返回在mm_date 上的最低价格在min_price - max_price 范围内的站点列表。您还可以使用如下查询查询多个日期:

q = Site.objects.values('name', 'category__product__price__date') \
        .annotate(min_price=Min('category__product__price__price')) \
        .filter(min_price__gte=min_price, min_price__lte=max_price)

2。渴望/预计算,你可以使用post_save 信号。由于写入频率较低,因此不会很昂贵

创建另一个表格来保存每个日期的最低价格。像这样:
    class LowestPrice(models.Model):
        date = models.DateField()
        site = models.ForeignKey(Site)
        lowest_price = models.IntegerField(default=0)
每次使用post_save 信号计算和更新它。示例代码(未测试)
    from django.db.models.signals import post_save
    from django.dispatch import receiver

    @receiver(post_save, sender=Price)
    def update_price(sender, instance, **kwargs):
        cur_price = LowestPrice.objects.filter(site=instance.product.category.site, date=instance.date).first()
        if not cur_price:
            new_price = LowestPrice()
            new_price.site = instance.product.category.site
            new_price.date = instance.date
        else:
            new_price = cur_price
        # update price only if needed
        if instance.price<new_price.lowest_price:
            new_price.lowest_price = instance.price
            new_price.save()
然后在需要的时候直接从这个表中查询:
    LowestPrice.objects.filter(date=mm_date, lowest_price__gte=min_price, lowest_price__lte=max_price)

【讨论】:

【参考方案2】:

我认为这个 ORM 查询可以完成这项工作......

from django.db.models import Min

sites = Site.objects.annotate(price_min= Min('category__product__price'))
            .filter(category__product__price=mm_date).unique().order_by('price_min')

或 /and 用于颠倒顺序:

sites = Site.objects.annotate(price_min= Min('category__product__price'))
            .filter(category__product__price=mm_date).unique().order_by('-price_min')

【讨论】:

【参考方案3】:

解决方案

from django.db.models import Min

Site.objects.annotate(
    price_min=Min('categories__products__prices__price')
).filter(
    categories__products__prices__date=the_date,
).distinct().order_by('price_min')   # prefix '-' for descending order

为此,您需要通过将related_name 属性添加到 ForeignKey 字段来修改模型。

像这样 -

class Category(models.Model):
    # rest of the fields
    site = models.ForeignKey(Site, related_name='categories')

类似地,对于 Product 和 Price 模型,在 ForeignKey 字段中添加 related_name 作为 productsprices

解释

related_name开头,描述了从一个模型到另一个模型的反向关系。 建立反向关系后,您可以使用它们对表进行内连接。 您可以使用反向关系来获取网站上某个类别的产品价格和annotate 的最低价格,由the_date 过滤。我已使用注释值按产品的最低价格按升序排序。您可以使用'-'作为前缀字符以降序执行。

【讨论】:

【参考方案4】:

使用 django 查询集操作来实现

Price.objects.all().order_by('price') #add [0] for only the first object

Price.objects.all().order_by('-price') #add [0] for only the first object

Price.objects.filter(date= ... ).order_by('price') #add [0] for only the first object

Price.objects.filter(date= ... ).order_by('-price') #add [0] for only the first object

Price.objects.filter(date= ... , price__gte=lower_limit, price__lte=upper_limit ).order_by('price') #add [0] for only the first object

Price.objects.filter(date= ... , price__gte=lower_limit, price__lte=upper_limit ).order_by('-price') #add [0] for only the first object

【讨论】:

我认为你没有理解这个问题。我不想要价目表我想要网站列表 那么您的 lamda 表达式似乎是正确的 - 如果它有效,则无需更改它。

以上是关于Django 复杂过滤器和排序的主要内容,如果未能解决你的问题,请参考以下文章

Django ListView - 过滤和排序的表单

Django ORM 按过滤器排序

Django:将过滤(和排序)添加到基于(通用)类的ListView的最佳方法?

Django ORM:按相关表过滤器的聚合排序

Django 1.9:为 QuerySet 创建复杂的自定义过滤器方法

Django DRF:过滤&搜索&排序功能