2015-12-17 14 views
9

Ich benutze Django 1.9 mit seinem eingebauten JSONField und Postgres 9.4. In meinem Modell attrs JSON-Feld ich Objekte mit einigen Werten, einschließlich Zahlen. Und ich muss über sie aggregieren, um Min/Max-Werte zu finden. Etwas wie folgt aus:Wie aggregiert man (min/max usw.) über Django JSONField Daten?

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

Auch wäre es sinnvoll sein, bestimmte Tasten zu extrahieren:

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

Die obigen Abfragen fehl mit FieldError: „Kann nicht Stichwort lösen‚my_key‘in Feld Join auf‚attrs. ' nicht gestattet."

Ist es irgendwie möglich?

Hinweise: 1) Ich weiß, wie Klar Postgres Abfrage macht die Arbeit zu tun, damit ich speziell für ORM-Lösung zu suchen, die Fähigkeit, filtern usw. 2) Ich nehme an, dies mit (relativ) neuen getan werden kann, Abfrage Ausdrücke/Lookups API, aber ich habe es noch nicht untersucht.

+0

Die Antwort unten ist gut. Eine andere nützliche Seite, die dies diskutiert, kann [hier] gefunden werden (http://hatethatcode.com/writing-queries-for-django-models-with-jsonfield.html). – eykanal

Antwort

13

Für diejenigen, die interessiert sind, habe ich die Lösung (oder Workaround mindestens) gefunden.

from django.db.models.expressions import RawSQL 

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

Beachten Sie, dass attrs->>%s Ausdruck geworden smth wie attrs->>'width' nach der Verarbeitung (ich meine einfache Anführungszeichen). Also, wenn Sie diesen Namen fest codieren, sollten Sie daran denken, sie einzufügen, sonst erhalten Sie einen Fehler.

/// Ein wenig offtopic ///

Und noch ein heikles Thema nicht im Zusammenhang selbst django aber das benötigt wird irgendwie behandelt werden. Als attrs ist JSON-Feld und es gibt keine Einschränkungen für seine Schlüssel und Werte Sie können (abhängig von Ihrer Anwendungslogik) einige nicht numerische Werte in zum Beispiel width Schlüssel. In diesem Fall erhalten Sie DataError von Postgres als Ergebnis der Ausführung der obigen Abfrage. NULL-Werte werden zwischenzeitlich ignoriert, also ist es in Ordnung. Wenn Sie den Fehler nur dann fangen können, kein Problem, Sie haben Glück. In meinem Fall musste ich falsche Werte ignorieren und der einzige Weg hier ist, benutzerdefinierte Postgres-Funktion zu schreiben, die Casting-Fehler unterdrücken wird.

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; 

Und dann verwenden, um Text in Zahlen zu werfen:

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

So erhalten wir recht solide Lösung für solch eine dynamische Sache wie json.

+0

Die Django-Dokumente erklären nicht wirklich, wie der sql, den Sie schreiben, funktioniert. In den Dokumenten benennen sie explizit die Tabelle, aus der Sie auswählen. Gibt es eine andere Dokumentation, in der detailliert beschrieben wird, was Sie tun können und was nicht? –

+0

Ich entdeckte, dass, wenn die Feldnamen camel case sind, maskierte doppelte Anführungszeichen eingeschlossen werden müssen. Ich entdeckte auch, dass :: numeric fehlgeschlagen ist, aber Cast (... wie numerisch) funktioniert hat. Beispiel ... _annotations = { '_cashTotal': RawSQL ("cast (\" payinJson \ "- >>% s als numerisch)", ("cashTotal",)), '_driverFuel': RawSQL ("cast (\ "payinJson \" - >>% s als numerisch) ", (" driverFuel ",)), '_fuelAmount': RawSQL (" cast (\ "payinJson \" - >>% s als numerisch) ", (" fuelAmount ",)) } –

0

Scheint, es gibt keinen nativen Weg, es zu tun.

arbeitete ich so um:

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

Diese weit davon entfernt, wunderbar, da es an der Python-Ebene (und nicht auf SQL-Ebene) durchgeführt wird.

16

Von django 1.11 (das ist noch nicht, so könnte sich das ändern) können Sie django.contrib.postgres.fields.jsonb.KeyTextTransform anstelle von RawSQL verwenden.

In django 1.10 müssen Sie KeyTransform zu Ihrem eigenen KeyTextTransform kopieren und den Operator -> durch ->> und #> durch #>> ersetzen, also gibt es Text anstelle von json Gegenständen zurück.

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

Sie können sogar KeyTextTransform s in SearchVector s für Volltextsuche

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

Denken Sie daran, sind Sie in jsonb Felder auch Index, so dass Sie, dass auf Ihren spezifischen Workload basierend berücksichtigen sollten.

+3

Danke dafür. Ich brauchte eine Weile, um zu entschlüsseln, wie man das benutzt. In meinem Fall werden die JSON-Daten in einem Modellfeld 'jdata' (äquivalent zu 'attrs' in der Frage) gespeichert und der json-Schlüssel ist 'createdDate', was die oberste Ebene ist. min_result = Modell.objects.annotate (val = KeyTextTransform ('createdDate', 'jdata')). Aggregat (min = Min ('val')) –

1

Ich weiß, das ist ein bisschen spät (mehrere Monate), aber ich stieß auf den Pfosten, während ich versuchte, dies zu tun. Managed zu tun es durch:

1) KeyTextTransform mit dem jsonb Wert konvertieren Cast in Text

2) unter Verwendung es zu konvertieren integer, so dass die Summe funktioniert:

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

print(q) 

wo ' data 'ist die jsonb-Eigenschaft und' numeric_val 'ist der Name der Variablen, die ich durch Annotation erstelle.

Hoffe das hilft jemand!

+0

Korrektur zu meinem Beitrag! Es sieht so aus, als müssten Sie einen zusätzlichen ersten Schritt zum Hinzufügen einer Anmerkung zum Darstellen ausführen. ' q = myModel.objects.filter (Typ = 8) \ .annotate (data_number = KeyTextTransform (sum_field, 'Daten')) \ .annotate (numeric_val = Besetzung ('data_number', Integer())) \ .aggregate (Summe ('numeric_val')) ' – Duncan

Verwandte Themen