J'ai deux modèles A
et B
. Tous les objets B
ont une clé étrangère vers un objet A
. Étant donné un ensemble d'objets A
, existe-t-il de toute façon d'utiliser l'ORM pour obtenir un ensemble d'objets B
contenant l'objet le plus récent créé pour chaque objet A
.
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 recherche donc une requête qui renvoie le gâteau le plus récent cuit dans chaque boulangerie à Anytown, aux États-Unis.
Pour autant que je sache, il n'y a pas de méthode en une seule étape pour le faire dans Django ORM.
Mais vous pouvez le diviser en deux requêtes:
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 lever l'ambiguïté du code ci-dessus (dans le cas où deux gâteaux arrivent en même temps, vous pouvez les obtenir tous les deux):
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)
Le mérite de BTW en revient à Daniel Roseman, qui a déjà répondu à une question similaire:
Si la méthode ci-dessus est trop lente, je connais également la deuxième méthode - vous pouvez écrire du SQL personnalisé produisant uniquement les gâteaux, qui sont les plus chauds dans les boulangeries pertinentes, le définir comme base de données VIEW, puis écrire un modèle Django non géré pour cela . Il est également mentionné dans le fil ci-dessus des utilisateurs de Django. Le lien direct vers le concept original est ici:
J'espère que cela t'aides.
A partir de Django 1.11
et grâce à Subquery et OuterRef et nous pouvons enfin construire un latest-per-group
requête 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))
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 disent, 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.
Cela devrait faire le travail:
from Django.db.models import Max
Bakery.objects.annotate(Max('cake__baked_at'))
Je me battais avec un problème similaire et j'ai finalement trouvé la solution suivante. Il ne repose pas sur order_by
et distinct
peuvent donc être triés comme vous le souhaitez du côté db et peuvent également être utilisés comme requête imbriquée pour le filtrage. Je crois également que cette implémentation est indépendante du moteur db, car elle est basée sur la clause sql HAVING
standard. Le seul inconvénient est qu'il retournera plusieurs gâteaux les plus chauds par boulangerie, s'ils sont cuits dans cette boulangerie exactement en même temps.
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'))
Cake.objects.filter(bakery__town="Anytown").order_by("-created_at")[:1]
Je n'ai pas construit les modèles de mon côté, mais en théorie, cela devrait fonctionner. En panne:
Cake.objects.filter(bakery__town="Anytown")
Devrait retourner tous les gâteaux qui appartiennent à "Anytown", en supposant que le pays ne fait pas partie de la chaîne. Les doubles soulignements entre bakery
et town
nous permettent d'accéder à la propriété town
de bakery
..order_by("-created_at")
classera les résultats par date de création, la plus récente en premier (notez le signe -
(moins) dans "-created_at"
. Sans le signe moins, ils être ordonné du plus ancien au plus récent.[:1]
À la fin ne renverra que le 1er élément de la liste qui est retourné (qui serait une liste de gâteaux d'Anytown, triés par les plus récents en premier).Remarque: Cette réponse est pour Django 1.11. Cette réponse a été modifiée à partir des requêtes affichées ici dans Django 1.11 Docs .