web-dev-qa-db-fra.com

Annotation de requête Django avec un champ booléen

Supposons que j'ai un modèle Product avec des produits dans une vitrine et un tableau ProductImages avec des images du produit, qui peuvent avoir zéro ou plusieurs images. Voici un exemple simplifié:

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

Lors de l'affichage des résultats de recherche de produits, je souhaite hiérarchiser les produits auxquels des images sont associées. Je peux facilement obtenir le nombre d'images:

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

Mais ce n'est pas vraiment ce que je veux. Je voudrais l'annoter avec un champ booléen, have_images, indiquant si le produit a une ou plusieurs images, afin que je puisse trier par ce biais:

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

Comment puis je faire ça? Merci!

24
Brett Gmoser


J'ai finalement trouvé un moyen de faire cela en utilisant le nouveau expressions conditionnelles de Django 1.8 :

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

Et c'est comme ça que j'ai finalement trouvé une incitation à passer de 1,7 à 1,8.

38
Brett Gmoser

Lire la documentation sur extra

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

Testé cela fonctionne

Mais order_by ou where (filtre) de ce champ ne me convient pas (Django 1.8) 0o:

Si vous avez besoin de commander le jeu de requêtes résultant en utilisant certains des nouveaux les champs ou les tables que vous avez inclus via extra () utilisent order_by paramétrez sur extra () et transmettez une séquence de chaînes. Ces cordes doivent être des champs de modèle (comme dans la méthode normale order_by () sur querysets), de la forme nom_table.nom_colonne ou un alias pour un colonne que vous avez spécifiée dans le paramètre select à extra ().

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

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

FieldError: impossible de résoudre le mot clé 'has_images' dans le champ.

J'ai trouvé https://code.djangoproject.com/ticket/19434 toujours ouvert.

Donc, si vous avez des problèmes comme moi, vous pouvez utiliser raw

1
madzohan

Si les performances comptent, ma suggestion est d'ajouter le champ hasPictures boolean (sous la forme editable=False

Puis conservez la valeur correcte dans ProductImagesignaux du modèle (ou en écrasant les méthodes save et delete

Avantages:

  • Index amical.
  • Meilleure performance. Évitez les jointures.
  • Base de données agnostique.
  • Le coder augmentera vos compétences Django au prochain niveau. 
1
dani herrera

Utilisez des expressions conditionnelles et convertissez outputfield en 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')
1
noufal valapra

A partir de Django 1.11, il est possible d'utiliser Exists. L'exemple ci-dessous provient de 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))
0
proxy

Lorsque vous devez annoter l'existence avec certains filtres, vous pouvez utiliser l'annotation Sum. Par exemple, après annote s'il existe des GIF dans images:

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

Cela les comptera en réalité, mais tout if product.animated_images: Pythonic fonctionnera comme il était booléen.

0
Ivan Klass