Аннотации запросов Django с булевым полем

Скажем, у меня есть модель Product с продуктами в витрине магазина и таблица ProductImages с изображениями продукта, которые могут иметь ноль или более изображений. Здесь упрощенный пример:

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')

Как я могу это сделать? Спасибо!

Ответы

Ответ 1


В итоге я нашел способ сделать это, используя django 1.8 new условные выражения:

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,8 от 1,7.

Ответ 2

Прочитайте документы о extra

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

Протестировано это работает

Но order_by или where (фильтр) этим полем не для меня (Django 1.8) 0o:

Если вам нужно заказать результирующий набор запросов, используя некоторые из новых поля или таблицы, которые вы включили через extra(), используйте order_by параметр extra() и передать последовательность строк. Эти строки должны быть либо полями модели (как в обычном порядке order_by() на querysets), формы table_name.column_name или псевдонима для который вы указали в параметре select для extra().

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

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

FieldError: не удается разрешить ключевое слово has_images в поле.

Я обнаружил, что https://code.djangoproject.com/ticket/19434 все еще открыт.

Итак, если у вас такие проблемы, как я, вы можете использовать raw

Ответ 3

Если производительность имеет значение, мое предложение состоит в том, чтобы добавить hasPictures логическое поле (как editable=False)

Затем сохраните правильное значение через ProductImage сигналы модели (или переписывая методы save и delete)

Преимущества:

  • Индекс дружественный.
  • Лучшая производительность. Избегайте присоединения.
  • Агностик базы данных.
  • Кодирование поднимет ваши навыки джанго до следующего уровня.

Ответ 4

Когда вам нужно аннотировать существование с некоторыми фильтрами, можно использовать аннотацию 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: будет работать так же, как и логическая.

Ответ 5

Используйте условные выражения и приведите выходное поле к BooleanField

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')

Ответ 6

Начиная с Django 1.11 можно использовать Exists. Пример ниже взят из документации Exists:

>>> 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))