Django, query ed espressioni condizionali

Pubblicatodi il Ago 31, 2018 in Programmazione
Nessun commento

Riprendendo dal precedente post in cui ho spiegato come ho fatto a migliorare drasticamente le performance della sezione statistiche del mio software, voglio fare oggi una piccola aggiunta.

Riprendendo sempre l’esempio del calcolo della media eseguito direttamente dal DBMS voglio aggiungere anche l’utilizzo di espressioni condizionali che permettono di risparmiare ulteriore codice Python.

In postgresql è possibile utilizzare all’interno delle query espressioni condizionali, banalmente if, else, che ci permettono di ritornare un risultato piuttosto che un altro in base al verificarsi o meno di una condizione.

Non starò ora a dilungarmi su come questo possa essere fatto tramite una query raw ma mi concentrerò solo ed esclusivamente su come realizzare questo utilizzando l’ORM di django.

La situazione in cui mi sono ritrovato è quella in cui dovevo sì calcolare la media di un valore totale ma questa media doveva essere calcolata in modi differenti in base al verificarsi o meno di determinate condizioni.

Chiaramente questo faceva sì che in casi come questi non potessi più delegare il calcolo della media al DMBS e che quindi l’unica soluzione fosse quella di estrarre il totale dalla query e poi, lato python, eseguire una serie di if per capire quale parametro utilizzare nel calcolo.

Per fortuna django viene in soccorso anche in questo caso. La query che ho utilizzato per fare questo è la seguente:

Click.objects.annotate(month=ExtractMonth('created')).values('month').annotate(day=ExtractDay('created')).annotate(tot=Count('id')).values('month', 'day', 'tot').annotate(avg=Case(When(created__month__lt=datetime.now().month, then=ExpressionWrapper(1.0 * F('tot') / ((datetime.now().year - 2016) + 1), output_field=FloatField())), When(created__month__gte=datetime.now().month, then=ExpressionWrapper(1.0 * F('tot') / (datetime.now().year - 2016), output_field=FloatField())), default=Value(-1), output_field=FloatField())).values('month', 'day', 'tot', 'avg').order_by('month', 'day', 'tot', 'avg')

Questa query mi permette di estrarre dal database il totale dei click ricevuti già suddivisi per mese e con la media già calcolata con il parametro corretto al verificarsi o meno di una determinata condizione.

Concentriamoci sull’ultima parte della query:

.annotate(avg=Case(When(created__month__lt=datetime.now().month, then=ExpressionWrapper(1.0 * F('tot') / ((datetime.now().year - 2016) + 1), output_field=FloatField())), When(created__month__gte=datetime.now().month, then=ExpressionWrapper(1.0 * F('tot') / (datetime.now().year - 2016), output_field=FloatField())), default=Value(-1), output_field=FloatField()))

Utilizzando il costrutto Case sto dicendo all’ORM che voglio tenere sotto controllo il verificarsi di alcune condizioni.

Per specificare quali siano queste condizioni (banalmente, i miei if) devo usare il costrutto When.

Il mio primo if è così composto:

When(created__month__lt=datetime.now().month, then=ExpressionWrapper(1.0 * F('tot') / ((datetime.now().year - 2016) + 1), output_field=FloatField()))

Indico al DMBS che quando il mese contenuto all’interno del campo created (che è un campo di tipo datetime) è minore del mese attuale in cui mi trovo allora (then) eseguo il calcolo della media secondo la formula riportata. Infine indico che il risultato deve essere restituito sotto forma di campo Float.

Possiamo chiaramente definire tutta una serie di When è il DMBS li andrà a verificare in sequenza. Un po’ come accade con i costrutti if usati normalmente.

Se nessuna di queste condizioni si verifica allora viene restituito il valore riportato nella variabile default:

default=Value(-1)

Nel mio caso ho deciso di voler ritornare un semplice -1 in quanto mi torna comodo per verificare la presenza di eventuali errori.

# # #

Rispondi