Django-Abfrage, die die neuesten Objekte aus verschiedenen Kategorien abruft

79

Ich habe zwei Modelle Aund B. Alle BObjekte haben einen Fremdschlüssel für ein AObjekt. Gibt es bei einer Reihe von AObjekten ohnehin die Möglichkeit, mit dem ORM eine Reihe von BObjekten abzurufen, die das zuletzt für jedes AObjekt erstellte Objekt enthalten.

Hier ist ein vereinfachtes Beispiel:

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

Ich suche also nach einer Abfrage, die den neuesten Kuchen zurückgibt, der in jeder Bäckerei in Anytown, USA, gebacken wurde.

Zach
quelle
6
Ich würde das auch gerne sehen :-)
gruszczy

Antworten:

35

Soweit ich weiß, gibt es in Django ORM keinen einstufigen Weg, dies zu tun.

Sie können es jedoch in zwei Abfragen aufteilen:

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

Wenn die IDs von Kuchen zusammen mit den Zeitstempeln von bake_at fortschreiten, können Sie den obigen Code vereinfachen und eindeutig definieren (falls zwei Kuchen gleichzeitig ankommen, können Sie beide erhalten):

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)

Übrigens geht das an Daniel Roseman, der einmal eine ähnliche Frage von mir beantwortet hat:

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

Wenn die obige Methode zu langsam ist, kenne ich auch die zweite Methode - Sie können benutzerdefiniertes SQL schreiben, das nur die Kuchen erzeugt, die in relevanten Bäckereien am heißesten sind, sie als Datenbankansicht definieren und dann ein nicht verwaltetes Django-Modell dafür schreiben. Es wird auch im obigen Thread für Django-Benutzer erwähnt. Der direkte Link zum ursprünglichen Konzept ist hier:

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

Hoffe das hilft.

Tomasz Zieliński
quelle
Ich werde wahrscheinlich mit den zweiten von Ihnen vorgeschlagenen Abfragen fortfahren. Vielen Dank.
Zach
Dies wäre effizienter, wenn Sie für die erste Abfrage eine Werteliste verwenden würden: 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)
dbn
Wenn Sie PostGreSQL verwenden, gibt es eine einstufige Lösung.
dbn
2
Erzeugt die erste Lösung nicht ein Problem, bei dem das späteste Datum für eine vor dem spätesten Datum einer anderen liegt, aber in einer anderen vorhanden ist? A = [1, 2, 3], B = [1, 2]. A spätestens = 3, B spätestens = 2. Die erste Abfrage scheint A's 2 und 3 sowie B's 2 zu bekommen.
kaungst
1
Ab Django 1.11jetzt gibt es einen Einwegschritt. Überprüfen Sie meine Antwort.
Todor
32

Ausgehend von Django 1.11und dank Subquery und OuterRef können wir endlich eine latest-per-groupAbfrage mit dem erstellen 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))
Todor
quelle
Dies funktionierte perfekt, obwohl mein Anwendungsfall etwas anders war. Was mir an diesem Ansatz gefällt, ist 1) er behält den resultierenden Abfragesatz in der Zielmodellinstanz bei und 2) er schließt Modellinstanzen nicht aus, die keine verwandten Daten haben (im Kontext der Frage Bäckereien, die keine haben) noch nichts gebacken).
Supra621
Sie sind der klügste Kerl
Yang Zhou
19

Wenn Sie zufällig PostGreSQL verwenden, können Sie die Django-Oberfläche verwenden, um EINZELTEILE ZU UNTERSCHEIDEN :

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

Wie in den Dokumenten angegeben , müssen Sie order bydieselben Felder wie Sie verwenden distinct on. Wie Simon weiter unten ausgeführt hat, müssen Sie zusätzliche Sortierungen im Python-Raum vornehmen.

dbn
quelle
Ich liebe den Ansatz - danke. Habe gerade eine kleine Korrektur bezüglich der endgültigen Bestellung vorgenommen. Abhängig von der Gesamtgröße des QS kann dies besser oder schlechter sein als die akzeptierte Antwort. In meinem Fall: besser :)
Simon Steinberger
Ich denke, das ist eine unnötige Komplikation des Codes und geht über das hinaus, was beantwortet wird. Ich gehe davon aus, dass die Leute herausfinden können, wie die resultierenden Daten sortiert werden.
dbn
Ich habe viel mit ähnlichen Problemen gespielt, MaxAnnotationen ausprobiert und nach ihnen gefiltert, aber sie scheiterten schließlich auf der DB-Seite, weil SQL nicht korrekt war, nachdem der Django-Optimierer order_by entfernt hatte (bei Verwendung des Ergebnisses als Filter-Unterabfrage oder beim Aggregieren, z .count(). B. ). Diese Lösung bricht nicht alle Dinge beim Abrufen recent_cakes.count()und wirft dabei keine Fehler Cake.objects.filter(pk__in=recent_cackes).filter(other_conditions), aber das neueste Beispiel gibt zufällige Kuchen pro Bäckerei zurück, die andere Bedingungen erfüllen (nicht die heißesten!), Da Django order_byaus der Unterabfrage entfernt :(
Ivan Klass
Ja, aus diesem Grund denke ich, dass die Antwort von Tomasz Zielinski der richtige Weg ist, wenn Sie nicht postGreSQL verwenden.
dbn
5

Dies sollte den Job machen:

from django.db.models import Max
Bakery.objects.annotate(Max('cake__baked_at'))
Daniel Roseman
quelle
5
Ich habe noch nicht getestet, aber das sieht so aus, als würde es die Zeit kommentieren, zu der jede Bäckerei zuletzt einen Kuchen gebacken hat. Ich suche nach den eigentlichen Kuchenobjekten. Interpretiere ich Ihre Antwort falsch?
Zach
Ja, du hast Recht. Ich hatte die vorherige Antwort vergessen, die ich für Tomasz gepostet hatte :-)
Daniel Roseman
1
Ich glaube, dass dies nur funktioniert, wenn die Sortierung der Kuchen nach IDs und Datum dieselbe Reihenfolge ergibt. In einem generischen Fall, in dem die Primärschlüsselfolge nicht der durch das Datumsfeld definierten chronologischen Reihenfolge entspricht, funktioniert dies nicht.
Dmitry B.
3

Ich kämpfte mit einem ähnlichen Problem und kam schließlich zu einer folgenden Lösung. Es ist nicht abhängig von order_byund distinctkann daher auf der Datenbankseite wie gewünscht sortiert und auch als verschachtelte Abfrage zum Filtern verwendet werden. Ich glaube auch, dass diese Implementierung unabhängig von der DB-Engine ist, da sie auf der Standard-SQL- HAVINGKlausel basiert . Der einzige Nachteil ist, dass mehrere heißeste Kuchen pro Bäckerei zurückgegeben werden, wenn sie genau zur gleichen Zeit in dieser Bäckerei gebacken werden.

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'))
Ivan Klass
quelle
0
Cake.objects.filter(bakery__town="Anytown").order_by("-created_at")[:1]

Ich habe die Modelle an meinem Ende nicht ausgebaut, aber theoretisch sollte dies funktionieren. Heruntergebrochen:

  • Cake.objects.filter(bakery__town="Anytown")Sollte alle Kuchen zurückgeben, die zu "Anytown" gehören, vorausgesetzt, das Land ist nicht Teil der Zeichenfolge. Das Doppelte unterstreicht zwischen bakeryund townermöglicht uns den Zugriff auf das townEigentum von bakery.
  • .order_by("-created_at")Die Ergebnisse werden nach dem Erstellungsdatum sortiert, dem letzten zuerst (beachten Sie das -(Minus-) Anmelden "-created_at". Ohne das Minuszeichen werden sie nach dem ältesten bis zum neuesten sortiert.
  • [:1] Am Ende wird nur das erste Element in der Liste zurückgegeben, das zurückgegeben wird (dies wäre eine Liste der Kuchen aus Anytown, sortiert nach dem letzten zuerst).

Hinweis: Diese Antwort gilt für Django 1.11. Diese Antwort wurde gegenüber den hier in Django 1.11 Docs gezeigten Abfragen geändert .

twknab
quelle
0

@Tomasz Zieliński Lösung oben hat Ihr Problem gelöst, aber es hat meins nicht gelöst, weil ich den Kuchen noch filtern muss. Hier ist meine Lösung

from django.db.models import Q, Max

hottest_yellow_round_cake = Max('cake__baked_at', filter=Q(cake__color='yellow', cake__shape='round'))

bakeries = Bakery.objects.filter(town='Chicago').annotate(
    hottest_cake_baked_at=hottest_yellow_round_cake
)

hottest_cakes = Cake.objects.filter(
    baked_at__in=[b.hottest_cake_baked_at for b in bakeries]
)

Mit diesem Ansatz können Sie auch andere Dinge wie Filter, Bestellung, Paginierung für Kuchen implementieren

Nguyen Anh Vu
quelle