2015-12-17 13 views
9

Sto usando django 1.9 con il suo JSONField integrato e postgres 9.4. Nel campo json attrs del mio modello, memorizzo oggetti con alcuni valori, compresi i numeri. E ho bisogno di aggregare su di loro per trovare valori min/max. Qualcosa di simile a questo:Come aggregare (min/max ecc.) Su dati Django JSONField?

Model.objects.aggregate(min=Min('attrs__my_key')) 

Inoltre sarebbe utile per estrarre i tasti specifici:

Model.objects.values_list('attrs__my_key', flat=True) 

Le query precedenti non riescono con FieldError: "Impossibile risolvere parola chiave 'my_key' nel campo Join su 'attrs. ' non consentito."

È possibile in qualche modo?

Note: 1) So come fare domanda Postgres semplici per fare il lavoro in modo da cercare specificamente per soluzione ORM di avere la capacità di filtrare ecc 2) Suppongo che questo può essere fatto con (relativamente) nuovo query espressioni/lookups api ma non l'ho ancora studiato.

+0

La risposta sotto è buona. Un altro sito utile che discute di questo può essere trovato [qui] (http://hatethatcode.com/writing-queries-for-django-models-with-jsonfield.html). – eykanal

risposta

13

Per coloro che sono interessati, ho trovato la soluzione (o soluzione almeno).

from django.db.models.expressions import RawSQL 

Model.objects.annotate(
    val=RawSQL("((attrs->>%s)::numeric)", (json_field_key,)) 
).aggregate(min=Min('val') 

Si noti che l'espressione attrs->>%s diventerà smth come attrs->>'width' dopo l'elaborazione (intendo virgolette singole). Quindi se hai hardcode questo nome dovresti ricordarti di inserirli o otterrai un errore.

/// Un po 'offtopic ///

E ancora una questione spinosa non riferibili alla Django se stessa, ma che è necessario per essere maneggiato in qualche modo. Poiché attrs è un campo JSON e non ci sono restrizioni sulle sue chiavi e valori che è possibile (a seconda della logica dell'applicazione) ottenere alcuni valori non numerici, ad esempio, nella chiave width. In questo caso otterrete DataError da postgres come risultato dell'esecuzione della query precedente. I valori NULL saranno ignorati nel frattempo quindi va bene. Se riesci a cogliere l'errore, allora nessun problema, sei fortunato. Nel mio caso ho dovuto ignorare i valori errati e l'unico modo qui è di scrivere la funzione postgres personalizzata che sopprimerà gli errori di cast.

create or replace function safe_cast_to_numeric(text) returns numeric as $$ 
begin 
    return cast($1 as numeric); 
exception 
    when invalid_text_representation then 
     return null; 
end; 
$$ language plpgsql immutable; 

e poi usarlo per lanciare il testo nei numeri:

Model.objects.annotate(
    val=RawSQL("safe_cast_to_numeric(attrs->>%s)", (json_field_key,)) 
).aggregate(min=Min('val') 

Così otteniamo soluzione abbastanza solida per una cosa così dinamico come JSON.

+0

I documenti di django non spiegano veramente come funziona lo sql che stai scrivendo. Nei documenti, nominano esplicitamente la tabella da cui si sta selezionando. C'è qualche altra documentazione che descrive in dettaglio cosa puoi e non puoi omettere? –

+0

Ho scoperto che se i nomi dei campi sono cammelli, le citazioni doppie devono essere incluse. Ho anche scoperto che: numeric failed ma cast (... as numeric) ha funzionato. Esempio ... _annotations = { '_cashTotal': RawSQL ("cast (\" payinJson \ "- >>% s come numerico)", ("cashTotal",)), '_driverFuel': RawSQL ("cast (\ "payinJson \" - >>% s come numerico) ", (" driverFuel ",)), '_fuelAmount': RawSQL (" cast (\ "payinJson \" - >>% s come numerico) ", (" fuelAmount ",)) } –

0

Sembra che non ci sia un modo nativo per farlo.

ho lavorato in giro in questo modo:

my_queryset = Product.objects.all() # Or .filter()... 
max_val = max(o.my_json_field.get(my_attrib, '') for o in my_queryset) 

Questo è ben lungi dall'essere meraviglioso, dal momento che è fatto a livello Python (e non a livello di SQL).

16

Da django 1.11 (che non è ancora uscito, quindi questo potrebbe cambiare) è possibile utilizzare django.contrib.postgres.fields.jsonb.KeyTextTransform anziché RawSQL.

In django 1.10 è necessario copiare/incollare KeyTransform per il proprio KeyTextTransform e sostituire l'operatore -> con ->> e #> con #>> in modo che restituisca il testo anziché gli oggetti JSON.

Model.objects.annotate(
    val=KeyTextTransform('json_field_key', 'blah__json_field')) 
).aggregate(min=Min('val') 

Si può anche includere KeyTextTransform s in SearchVector s per la ricerca a testo integrale

Model.objects.annotate(
    search=SearchVector(
     KeyTextTransform('jsonb_text_field_key', 'json_field')) 
    ) 
).filter(search='stuff I am searching for') 

Ricorda che puoi anche indice in campi jsonb, così si dovrebbe considerare che in base alla propria carico di lavoro specifico.

+3

Grazie per questo. Mi ci è voluto un po 'per decodificare come usare questo. Nel mio caso, i dati JSON sono memorizzati in un campo modello 'jdata' (equivalente a 'attrs' nella domanda) e la chiave JSON è 'createdDate' che è di primo livello. min_result = Model.objects.annotate (val = KeyTextTransform ('createdDate', 'jdata')). Aggregate (min = Min ('val')) –

1

So che è un po 'tardi (diversi mesi) ma ho trovato il post mentre cercavo di farlo. Riuscito a farlo:

1) utilizzando KeyTextTransform per convertire il valore jsonb al testo

2) utilizzando Fusioni per convertirlo in un intero, in modo che la somma funziona:

q = myModel.objects.filter(type=9) \ 
.annotate(numeric_val=Cast(KeyTextTransform(sum_field, 'data'), IntegerField())) \ 
.aggregate(Sum('numeric_val')) 

print(q) 

dove ' data 'è la proprietà jsonb e' numeric_val 'è il nome della variabile che creo annotando.

Spero che questo aiuti qualcuno!

+0

Correzione per il mio post! Sembra che tu debba fare un primo passo in più per aggiungere un'annotazione da trasmettere. ' q = myModel.objects.filter (tipo = 8) \ .annotate (data_number = KeyTextTransform (sum_field, 'dati')) \ .annotate (numeric_val = Cast ('data_number', IntegerField())) \ .aggregate (Sum ('numeric_val')) ' – Duncan

Problemi correlati