82 votes

Requête Django qui récupère les objets les plus récents de différentes catégories

J'ai deux modèles A y B . Tous les sites B ont une clé étrangère vers un A objet. Étant donné un ensemble de A est-il possible d'utiliser l'ORM pour obtenir un ensemble d'objets de type B contenant l'objet le plus récent créé pour chaque A objeto.

Voici un exemple simplifié :

class Bakery(models.Model):
    town = models.CharField(max_length=255)

class Cake(models.Model):
    bakery = models.ForeignKey(Bakery, on_delete=models.CASCADE)
    baked_at = models.DateTimeField()

Je cherche donc une requête qui renvoie le gâteau le plus récent fait dans chaque boulangerie de Anytown, aux États-Unis.

7 votes

J'aimerais bien voir ça aussi :-)

40voto

Todor Points 7800

À partir de Django 1.11 et grâce à Sous-requête y OuterRef nous pouvons enfin construire un latest-per-group en utilisant le ORM .

hottest_cakes = Cake.objects.filter(
    baked_at=Subquery(
        (Cake.objects
            .filter(bakery=OuterRef('bakery'))
            .values('bakery')
            .annotate(last_bake=Max('baked_at'))
            .values('last_bake')[:1]
        )
    )
)

#BONUS, we can now use this for prefetch_related()
bakeries = Bakery.objects.all().prefetch_related(
    Prefetch('cake_set',
        queryset=hottest_cakes,
        to_attr='hottest_cakes'
    )
)

#usage
for bakery in bakeries:
    print 'Bakery %s has %s hottest_cakes' % (bakery, len(bakery.hottest_cakes))

38voto

Tomasz Zielinski Points 9300

Pour autant que je sache, il n'y a pas de moyen unique de le faire dans l'ORM de Django, mais vous pouvez le diviser en deux requêtes :

from django.db.models import Max

bakeries = Bakery.objects.annotate(
    hottest_cake_baked_at=Max('cake__baked_at')
) 
hottest_cakes = Cake.objects.filter(
    baked_at__in=[b.hottest_cake_baked_at for b in bakeries]
)

Si les identifiants des gâteaux progressent avec les horodatages bake_at, vous pouvez simplifier et désambiguïser le code ci-dessus (dans le cas où deux gâteaux arrivent en même temps, vous pouvez obtenir les deux) :

from django.db.models import Max

hottest_cake_ids = Bakery.objects.annotate(
    hottest_cake_id=Max('cake__id')
).values_list('hottest_cak‌​e_id', flat=True)

hottest_cakes = Cake.objects.filter(id__in=hottest_cake_ids)

Le mérite en revient à Daniel Roseman, qui a déjà répondu à une question similaire de ma part :

http://groups.google.pl/group/django-users/browse_thread/thread/3b3cd4cbad478d34/3e4c87f336696054?hl=pl&q=

Si la méthode ci-dessus est trop lente, alors je connais aussi une deuxième méthode - vous pouvez écrire un SQL personnalisé produisant uniquement les gâteaux les plus chauds dans les boulangeries concernées, le définir en tant que VIEW de base de données, et ensuite écrire un modèle Django non géré pour cela. C'est également mentionné dans le fil de discussion django-users ci-dessus. Le lien direct vers le concept original est ici :

http://web.archive.org/web/20130203180037/http://wolfram.kriesing.de/blog/index.php/2007/django-nice-and-critical-article#comment-48425

J'espère que cela vous aidera.

0 votes

Je vais probablement opter pour la deuxième série de requêtes que vous avez suggérée. Merci.

0 votes

Ce serait plus efficace si vous utilisiez une liste de valeurs pour la première requête : hottest_cake_ids = Bakery.objects.annotate(hottest_cake_id=Max('cake__id')).values_list('hottest_cake_id', flat=True) ; hottest_cakes = Cake.objects.filter(id__in=hottest_cake_ids)

0 votes

En outre, si vous utilisez PostGreSQL, il existe une solution en une seule étape.

23voto

dbw Points 2246

Si vous utilisez PostGreSQL, vous pouvez utiliser L'interface de Django pour DISTINCT ON :

recent_cakes = Cake.objects.order_by('bakery__id', '-baked_at').distinct('bakery__id')

Comme les docs dire, vous devez order by les mêmes champs que vous distinct on . Comme Simon l'a souligné ci-dessous, si vous souhaitez effectuer un tri supplémentaire, vous devrez le faire dans l'espace Python.

0 votes

J'aime cette approche - merci. J'ai juste fait une petite correction concernant la commande finale. En fonction de la taille totale du QS, cela peut être meilleur ou pire que la réponse acceptée. Dans mon cas : meilleure :)

0 votes

Je pense que c'est une complication inutile du code et que cela va au-delà de ce qui est répondu. Je vais supposer que les gens peuvent trouver comment trier les données résultantes.

0 votes

J'ai beaucoup joué avec le même problème, en essayant Max et de filtrer sur elles, mais elles échouaient finalement du côté de la base de données à cause d'un sql incorrect après que l'optimiseur de django ait supprimé order_by (lors de l'utilisation du résultat comme sous-requête de filtre ou lors de l'agrégation, par ex. .count() ). Cette solution ne casse pas tout lors de l'extraction des données. recent_cakes.count() et ne lancent pas d'erreurs en faisant Cake.objects.filter(pk__in=recent_cackes).filter(other_condi‌​tions) ,mais le dernier exemple retourne au hasard gâteaux par boulangerie qui satisfont aux autres_conditions (pas les plus chaudes !), parce que django supprime les order_by de la sous-requête :(

5voto

Daniel Roseman Points 199743

Cela devrait faire l'affaire :

from django.db.models import Max
Bakery.objects.annotate(Max('cake__baked_at'))

5 votes

Je n'ai pas encore testé, mais il semble que cela annotera l'heure à laquelle chaque boulangerie a le plus récemment préparé un gâteau. Je suis à la recherche des objets gâteaux réels. Est-ce que j'interprète mal votre réponse ?

0 votes

Oui, vous avez raison. J'avais oublié la réponse précédente que j'ai postée pour Tomasz :-)

1 votes

Je pense que cela ne fonctionnera que si le tri des gâteaux par ids et par date donne le même ordre. Dans un cas générique où la séquence de la clé primaire ne correspond pas à l'ordre chronologique tel que défini par le champ date, cela ne fonctionnera pas.

4voto

Klass Ivan Points 1003

Je me suis battu avec un problème similaire et j'ai finalement trouvé la solution suivante. Elle ne repose pas sur order_by y distinct Il peut donc être trié comme on le souhaite du côté de la base de données et peut également être utilisé comme requête imbriquée pour le filtrage. Je pense également que cette implémentation est indépendante du moteur de la base de données, car elle est basée sur le langage sql standard. HAVING clause. Le seul inconvénient est qu'elle renvoie plusieurs gâteaux les plus chauds par boulangerie, s'ils sont cuits dans cette boulangerie exactement au même moment.

from django.db.models import Max, F

Cake.objects.annotate(
    # annotate with MAX "baked_at" over all cakes in bakery
    latest_baketime_in_bakery=Max('bakery__cake_set__baked_at')
    # compare this cake "baked_at" with annotated latest in bakery
).filter(latest_baketime_in_bakery__eq=F('baked_at'))

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X