2013-06-20 12 views
8

Ho un modello di base come:QuerySet con .latest() per ogni giorno

class Stats(models.Model): 

    created = models.DateTimeField(auto_now_add=True) 
    growth = models.IntegerField() 

esegue un processo di sedano ogni 10 minuti per creare un nuovo oggetto statistiche.

L'utilizzo di .latest() su un QuerySet mi fornisce l'ultimo oggetto Stat aggiornato.

Tuttavia, voglio una lista con un oggetto Stats per ogni giorno.

Si consideri il seguente:

Stats(growth=100) #created 1/1/13 23:50 
Stats(growth=200) #created 1/1/13 23:59 
Stats(growth=111) #created 1/2/13 23:50 
Stats(growth=222) #created 1/2/13 23:59 

Il QuerySet dovrebbe restituire il più recente per ogni giorno. Nell'esempio quello con crescita 200 e 222.

In SQL, accendere una subquery con il massimo per ogni giorno e unirle insieme.

Poiché non voglio utilizzare SQL raw, esiste un modo per farlo con l'ORM django?

+1

Giusto per chiarirmi la testa; se vuoi l'ultimo per ogni giorno - nel tuo esempio non vorresti la crescita del 200 e del 222? – Ewan

+0

sì, è vero. L'ho corretto;) – Jannis

risposta

4

Purtroppo non c'è modo (che io sappia .. ho guardato abbastanza duro) per evitare di utilizzare un po ' tipo di raw sql per realizzare ciò che si desidera fare (con il modello corrente; vedere la fine per un altro suggerimento). Ma puoi minimizzare l'impatto scrivendo il più piccolo sql raw possibile. In pratica i siti django non hanno bisogno di essere trasferiti su database diversi. A meno che non preveda di utilizzare questa app altrove o di pubblicarla pubblicamente, dovresti essere ok.

L'esempio seguente è per sqlite. È possibile mantenere una mappatura dei tipi di database sulle funzioni date, cercare il tipo di driver e sostituire la funzione con quella corretta se necessario.

>>> for stat in Stats.objects.all(): 
...  print stat.created, stat.growth 
... 
2013-06-22 13:41:25.334262+00:00 3 
2013-06-22 13:41:40.473373+00:00 3 
2013-06-22 13:41:44.921247+00:00 4 
2013-06-22 13:41:47.533102+00:00 5 
2013-06-23 13:41:58.458250+00:00 6 
2013-06-23 13:42:01.282702+00:00 3 
2013-06-23 13:42:03.633236+00:00 1 

>>> last_stat_per_day = Stats.objects.extra( 
      select={'the_date': 'date(created)' } 
     ).values_list('the_date').annotate(max_date=Max('created')) 

>>> last_stat_per_day 
[(u'2013-06-22', datetime.datetime(2013, 6, 22, 13, 41, 47, 533102, tzinfo=<UTC>)), (u'2013-06-23', datetime.datetime(2013, 6, 23, 13, 42, 3, 633236, tzinfo=<UTC>))] 

>>> max_dates = [item[1] for item in last_stat_per_day] 
>>> max_dates 
[datetime.datetime(2013, 6, 22, 13, 41, 47, 533102, tzinfo=<UTC>), 
datetime.datetime(2013, 6, 23, 13, 42, 3, 633236, tzinfo=<UTC>)] 

>>> stats = Stats.objects.filter(created__in=max_dates) 
>>> for stat in stats: 
...  print stat.created, stat.growth 
... 
2013-06-22 13:41:47.533102+00:00 5 
2013-06-23 13:42:03.633236+00:00 1 

avevo scritto qui prima che questa era solo una singola query, ma ho mentito - il values_list deve essere trasformato per restituire solo il MAX_DATE per la query successiva, il che significa eseguire l'istruzione. Tuttavia sono solo 2 query, che sarebbero significativamente migliori di una funzione N + 1.

Il bit non-portatile è questo:

last_stat_per_day = Stats.objects.extra( 
    select={'the_date': 'date(created)' } 
).values_list('the_date').annotate(max_date=Max('created')) 

Utilizzando extra non è l'ideale, ma l'SQL prime qui è semplice, e si presta bene per una sostituzione dipendente driver di database. È necessario sostituire solo lo date(created). Puoi completarlo in un metodo su un gestore personalizzato, se lo desideri, e hai quindi estratto con successo questo pasticcio in un'unica posizione.

L'altra opzione è quella di aggiungere un modello DateField al modello e quindi non è necessario utilizzare l'extra. Dovresti semplicemente sostituire la chiamata values_list con uno values_list('created_date'), rimuovere completamente lo extra e chiamarlo un giorno. Il costo è ovvio: è necessario più spazio di archiviazione.Inoltre, non è intuitivo il motivo per cui si dispone di un campo Date e DateTime sullo stesso modello. Mantenere i due in sincrono può anche porre problemi.

0

Forse si può fare somehting come:

import datetime 
day = datetime.datetime.now().day 
the_last_one = Stats.objects.filter(created__day=day).order_by('-created')[0] 

o qualcosa di simile

the_last_one = Stats.objects.filter(created__day=day).order_by('created').latest() 
+0

restituiranno l'ultimo oggetto Stats, non un elenco di oggetti con le ultime statistiche per ogni giorno. – Jannis

0

In aggiunta alle altre due risposte, è consigliabile prendere in considerazione la possibilità di archiviare i risultati in un altro modello (soprattutto se i dati giornalieri non cambiano molto dopo essere entrati e si dispone di grandi quantità di dati). Qualcosa di simile:

class DailyStat(models.Model): 
    date = models.DateField(unique=True) 
    # Denormalisation yo 
    # Could also store foreign keys to Stats instances if needed 
    max_growth = models.IntegerField() 
    min_growth = models.IntegerField() 
    # . 
    # . 
    # . 
    # and any other stats per day e.g. average per day 

E aggiungere un task Sedano periodica:

from celery.task.schedules import crontab 
from celery.task import periodic_task 
import datetime 

# Periodic task for 1am daily 
@periodic_task(run_every=crontab(minute=0, hour=1)) 
def process_stats_ery_day(): 
    # Code to populate DailyStat 
    today = datetime.date.today() 
    # Assumes relevant custom Manager methods exist 
    # Can use regular Django ORM methods to achieve this 
    max = Stats.objects.get_max_growth(date=today) 
    min = Stats.objects.get_min_growth(date=today) 
    ds = DailyStat(date=today, max_growth=max.growth, min_growth=min.growth) 
    ds.save() 

recuperare i risultati con:

DailyStat.objects.all() 

Naturalmente, tra gli altri fattori da considerare, questo approccio presenta il problema della dover aggiornare DailyStat quando una statistica passata cambia eccetera (signals può essere utilizzato se si prende questo percorso.)

0

TruncDate è nuovo in Django> 2.0 e ora è possibile ridurre la stessa query, ma solo in database con supporto distinct come PostgreSQL.

Stats.objects.all().annotate(date=TruncDay('created')).distinct('created').order_by('-date')

Problemi correlati