带有布尔字段的 Django 查询注释

Django query annotation with boolean field

假设我有一个 Product 模型,在店面中有产品,还有一个 ProductImages table 有产品图片,可以有零个或多个图片。这是一个简化的例子:

class Product(models.Model):
  product_name = models.CharField(max_length=255)
  # ...

class ProductImage(models.Model):
  product = models.ForeignKey(Product, related_name='images')
  image_file = models.CharField(max_length=255)
  # ...

在显示产品搜索结果时,我想优先显示具有相关图片的产品。我可以很容易地得到图像的数量:

from django.db.models import Count
Product.objects.annotate(image_count=Count('images'))

但这并不是我真正想要的。我想用一个布尔字段 have_images 来注释它,表明该产品是否有一张或多张图片,以便我可以按它排序:

Product.objects.annotate(have_images=(?????)).order_by('-have_images', 'product_name')

我该怎么做?谢谢!

阅读有关 extra

的文档
qs = Product.objects.extra(select={'has_images': 'CASE WHEN images IS NOT NULL THEN 1 ELSE 0 END' })

测试有效

但是 order_bywhere(filter) 这个字段不适合我 (Django 1.8) 0o:

If you need to order the resulting queryset using some of the new fields or tables you have included via extra() use the order_by parameter to extra() and pass in a sequence of strings. These strings should either be model fields (as in the normal order_by() method on querysets), of the form table_name.column_name or an alias for a column that you specified in the select parameter to extra().

qs = qs.extra(order_by = ['-has_images'])

qs = qs.extra(where = ['has_images=1'])

FieldError: Cannot resolve keyword 'has_images' into field.

我发现 https://code.djangoproject.com/ticket/19434 仍然打开。

所以如果你和我一样有这样的困扰,可以使用raw

如果性能很重要,我的建议是添加 hasPictures 布尔字段(如 editable=False

然后通过ProductImagemodel signals(或覆盖savedelete方法)

保留正确的值

优点:

  • 索引友好。
  • 更好的性能。避免连接。
  • 数据库不可知。
  • 编写代码将使您的 Django 技能更上一层楼。


我最终找到了一种方法来使用 django 1.8 的新 conditional expressions:

from django.db.models import Case, When, Value, IntegerField
q = (
    Product.objects
           .filter(...)
           .annotate(image_count=Count('images'))
           .annotate(
               have_images=Case(
                   When(image_count__gt=0,
                        then=Value(1)),
                   default=Value(0),
                   output_field=IntegerField()))
           .order_by('-have_images')
)

这就是我最终找到从 1.7 升级到 1.8 的动力的原因。

当你必须用一些过滤器来注释存在时,可以使用Sum注释。例如下面注释images中是否有GIF:

Product.objects.filter(
).annotate(
    animated_images=Sum(
        Case(
             When(images__image_file__endswith='gif', then=Value(1)),
             default=Value(0),
             output_field=IntegerField()
        )
    )
)

这实际上会计算它们,但是任何 pythonic if product.animated_images: 都将像布尔值一样工作。

使用条件表达式并将输出字段转换为布尔字段

Product.objects.annotate(image_count=Count('images')).annotate(has_image=Case(When(image_count=0, then=Value(False)), default=Value(True), output_field=BooleanField())).order_by('-has_image')

从 Django 1.11 开始,可以使用 Exists。下面的示例来自 Exists documentation:

>>> from django.db.models import Exists, OuterRef
>>> from datetime import timedelta
>>> from django.utils import timezone
>>> one_day_ago = timezone.now() - timedelta(days=1)
>>> recent_comments = Comment.objects.filter(
...     post=OuterRef('pk'),
...     created_at__gte=one_day_ago,
... )
>>> Post.objects.annotate(recent_comment=Exists(recent_comments))