Django-filter: Filtern Sie nach contrib.postgres JSONField

Erstellt am 4. Juni 2016  ·  16Kommentare  ·  Quelle: carltongibson/django-filter

Dies begann in der Google-Diskussionsgruppe:
https://groups.google.com/forum/#!topic/django-filter/RwNfoWsdeLQ

Ich bin daran interessiert, contrib.postgres JSONFields mit Django-Filter filtern zu können.

Ich habe einen Filter, der für ein paar Beispiele funktioniert. Das ist komplizierter als ich dachte, da Sie den Datentyp in Ihrem JSON nicht wirklich im Voraus kennen, wie Sie es mit etwas wie einem IntegerField tun. Vielleicht mache ich es mir nur zu kompliziert.

Hier ist ein Beispielfilter, der auf mein JSONField trifft
http://127.0.0.1 :8000/api/v1/craters?data= latitude:float :-57:lte~!@!~ age:str :PC

Hier sind die Modelle und der Filtercode:
https://gist.github.com/jzmiller1/627071f555186cd1a58bb8f065205ff7

Ich werde mich weiter damit herumschlagen. Wenn jemand eine Meinung oder ein Feedback hat, lass es mich wissen ...

Hilfreichster Kommentar

Ich denke, es wäre wirklich erstaunlich, einen JSONFilter zu haben, der Abfragen wie jsonfield__a_random_key=value ermöglicht. Ich weiß, dass Sie es mit der Methode objects.filter machen können. Vielleicht könnte der Kompromiss die Filtervalidierung sein?

Alle 16 Kommentare

Hallo @jzmiller1. Was genau versuchst du zu erreichen? Ich kann nicht sagen, ob Sie:

  • Versuchen Sie, einen generischen JSONFilter zu erstellen, mit dem Sie jedes beliebige Attribut in einem JSONField abfragen können. oder,
  • versuchen, bestimmte Attribute (Breitengrad, Alter) aufzudecken, die Ihrem Krater gemeinsam sind data . Diese Attribute wären im Wesentlichen das Schema Ihres data .

Ersteres ist interessant, aber wie Sie herausgefunden haben, liegt die Komplikation darin, dass das JSONField von Natur aus schemalos ist. Ohne ein Schema können Sie keinen Code schreiben, um Filter automatisch zu generieren. Ihr MethodFilter funktioniert dahingehend, dass er jede beliebige Attributsuche zulässt, aber Sie können diese Suchen nicht validieren . zB ?data=latitude:char:PC:isnull ist möglich, aber unsinnig. Welche Lösung hier auch immer einen Kompromiss erfordert. Ein völlig willkürlicher Filter kann die Suchvorgänge nicht validieren, ein validierender Filter würde eine Möglichkeit erfordern, ein Schema bereitzustellen.

Für den zweiten Fall sind die Lösungen ausführlich/mühsam, aber unkompliziert.

class CratersFilter(filters.FilterSet):
    latitude = filters.NumberFilter(name='data__latitude', lookup_expr='exact')
    latitude__lt = filters.NumberFilter(name='data__latitude', lookup_expr='lt')
    latitude__gt = filters.NumberFilter(name='data__latitude', lookup_expr='gt')
    latitude__isnull = filters.BooleanFilter(name='data__latitude', lookup_expr='isnull')
    # not sure if 'isnull' is a valid lookup for JSONFields - just demonstrating that 
    # different lookups expect different value types.

    age = filters.CharFilter(name='data__age', lookup_expr='exact')
    ...

Ihre Abfrage würde dann so aussehen:

http://127.0.0.1:8000/api/v1/craters?latitude__lte=-57&age=PC

Mein Ziel ist es, einen generischen JSONFilter zu erstellen, der Abfragen für beliebige Attribute in einem JSONField zulässt. Für das, woran ich arbeite, weiß ich nicht wirklich, was sich in den Daten für einen bestimmten Krater befindet, aber wenn es dort einen Schlüssel gibt, nach dem ich suche, würde ich ihn gerne abfragen können.

Was die Unfähigkeit betrifft, die Lookup-Typen zu validieren, denke ich, dass ich mich darauf verlassen würde, dass der Benutzer, der die Abfrage durchführt, erkennt, dass die Abfrage unsinnig ist, und es einfach nicht schafft, damit anzufangen.

Ich bin mir nicht sicher, ob das, was ich zu tun versuche, Zeitverschwendung ist oder nicht. Vielleicht gibt es eine bessere Lösung für das, was ich erreichen möchte. Ich war neugierig, ob jemand größere Probleme sah, die dies verhindern würden, oder ob jemand einen Anwendungsfall hatte, in dem dies nützlich wäre. Danke, dass du es dir angesehen hast!

Mein erster Gedanke gilt der Validierung gemäß dem Punkt von @rpkilby . Schemalos ist aus Entwicklersicht nett – aber ich bin mir nicht sicher, ob Sie es direkt mit adressierbaren URLs verbinden möchten.

Lasst uns das jetzt offen lassen. Ich sehe, dass es eine beliebte Anfrage ist. (Es würde sich also lohnen, es auf der Ebene von _"Hier ist ein Beispiel MethodFilter "_ in der Dokumentation anzusprechen.)

Für das, woran ich arbeite, weiß ich nicht wirklich, was sich in den Daten für einen bestimmten Krater befindet, aber wenn es dort einen Schlüssel gibt, nach dem ich suche, würde ich ihn gerne abfragen können.

Das scheint ... irgendwie irre. Sie stellen eine API für Kraterdaten bereit, wissen aber nicht, was in den von Ihnen bereitgestellten Daten enthalten ist? Meinen Sie damit, dass einigen Datensätzen Attribute eines gemeinsamen Schemas fehlen oder dass einzelne Datensätze völlig willkürlich sind?

Ich werde dies für den Moment als Außerhalb des Geltungsbereichs schließen. Berücksichtigen Sie gerne dokumentierte, getestete Pull-Requests. Möglicherweise haben wir die Kapazität, dies in Zukunft zu überdenken.

Ich denke, es wäre wirklich erstaunlich, einen JSONFilter zu haben, der Abfragen wie jsonfield__a_random_key=value ermöglicht. Ich weiß, dass Sie es mit der Methode objects.filter machen können. Vielleicht könnte der Kompromiss die Filtervalidierung sein?

Soeben wurde eine Implementierung einer „natürlichen“ Abfrage für den QuerySet-Filter mit dem Q-Objekt abgeschlossen. Es wurde anhand eines Abfragesatzes mit ~ 1000 Datensätzen unter Verwendung eines JsonField-Elements getestet. Umsetzung ist am:
https://github.com/shallquist/DJangoQuerySetFilter/blob/master/queryparser.py

Hey @shallquist , ich bin mir nicht sicher, wie man QuerySetFilter im Kontext des Django-Filters verwendet. Hast du die Nutzung irgendwo dokumentiert?

Es ist ziemlich einfach zu verwenden, wie in der Readme auf Github gezeigt wird. Normale Abfragen sollten unterstützt werden, dh.
QuerySetFilter('friends').get_Query((person__address__city = Denver | person__address__city = Boulder) & person__address_state ~= CO)
Dadurch wird eine Abfrage erstellt, um alle Freunde abzurufen, die in Dever oder Boulder Colorado leben, wobei Freunde ein jsonfield ist.

BTW Dies wurde nicht viel getestet und da Django-Filter das Abfragen eingebetteter Array-Objekte nicht unterstützen, habe ich diesen Ansatz aufgegeben.

https://github.com/carltongibson/django-filter/issues/426#issuecomment -380224133

Ich denke, es wäre wirklich erstaunlich, einen JSONFilter zu haben, der Abfragen wie jsonfield__a_random_key=value ermöglicht. Ich weiß, dass Sie dies mit der Methode objects.filter tun können. Vielleicht könnte der Kompromiss die Filtervalidierung sein?

Hey @carltongibson , @rpkilby Ich würde gerne deine Meinung dazu erfahren. Nehmen wir an, my_field ist ein Postgres JSONField , und ich möchte:

  • Fügen Sie einen REST-Filter in Form von my_field__etc=value , wobei etc eine der Abfragen ist, die von JSONField und value unterstützt werden, was auch immer der REST-Benutzer bereitstellt.
  • Dann möchte ich etc und value in Form von MyModel.objects.filter(my_field__etc=value) $ an den Model Objects Manager übergeben.
  • Rufen Sie schließlich alles ab, was der Filter zurückgibt.

Es scheint super trivial zu sein, aber ich habe nicht herausgefunden, wie man so etwas angeht. Wenn Sie mir einen kleinen Anhaltspunkt geben, könnte ich versuchen, es umzusetzen.

Alle mögliche Gedanken würden super geschätzt!

Funktioniert so etwas nicht?

class MyFilter(FilterSet):
    my_field__etc = filters.NumberFilter(field_name='my_field', lookup_expr='etc')

Im Allgemeinen sollte field_name mit dem zugrunde liegenden Modellfeldnamen übereinstimmen, während Transformationen und Suchvorgänge (in diesem Fall eine Schlüsseltransformation) in lookup_expr enthalten sein sollten.

@rpkilby vielen Dank für eine so schnelle Antwort - Ja genau, aber ich möchte, dass etc vom Benutzer in der Anfrage bereitgestellt werden ... Also konnte ich es nicht wirklich im Filter hartcodieren 💭

Der Filter sollte eher so aussehen:

class MyFilter(FilterSet):
    my_field = JSONFieldFilter(field_name='my_field')

Also ein einzelner JSON-Filter, um beliebige Abfrageparameter wie ?my_field__etc=value zu verarbeiten.

Ich sehe zwei Probleme. Erstens besteht ein Teil des Werts von django-filter darin, dass er die Abfrageparameter validiert. Da JSONField s kein Schema haben, ist es nicht möglich, Filter zu generieren, die die eingehenden Daten angemessen validieren. Wenn Ihr JSON-Feld beispielsweise einen "count"-Schlüssel hat, wäre es nicht möglich, zu erkennen, dass nur positive Zahlen gültig sind. Das Beste, was getan werden könnte, ist sicherzustellen, dass der Wert gültiges JSON ist. Die Abfragen wären also zumindest gültig, aber möglicherweise unsinnig (zB data__count__gt='cat' ).

Zweitens wird dieser Filter die gleichen Einschränkungen haben wie MultiWidget -basierte Filter. zB werden keine Validierungsfehler für die korrekten Parameternamen generiert. Aber bevor ich darauf eingehe, hier ist, wie ich den Filter wahrscheinlich implementieren würde. Wir brauchen:

  • Eine Filterklasse zur Durchführung der eigentlichen Filterung, die mehrere Parameter verarbeiten sollte
  • Ein Formularfeld zur Validierung der JSON-Daten
  • Ein Widget, um die Daten für die beliebigen my_field__* -Parameter zu erhalten.
class JSONWidget(widgets.Textarea):
    """A widget that handles multiple parameters prefixed with the field name."""

    def value_from_datadict(self, data, files, name):
        prefix = f'{name}{LOOKUP_SEP}'

        # this is doing two things: 
        # - matches multiple params for the base field name
        # - in addition to returning the value, we also need the full parameter name
        #   for querying. otherwise, values will be filtered against the base `name`. 
        return {k: v for k, v in data.items() if k.startswith(prefix)}

    def get_context(self, name, value, attrs):
        # to support rendering the widget, you would need to generate subwidgets
        # similar to MultiWidget.get_context.
        pass

class JSONField(postgres.forms.JSONField):
    widget = JSONWidget

    def clean(self, value):
        # note that it's not possible to collect/reraise any validation errors under
        # their actual parameter names. `form.add_error` should be used here, however
        # the field class does not have access to the form instance. raising 
        # ValidationError({k: str(original_exc)}) also does not work. 

        # clean/convert each value
        return {k: super().clean(v) for k, v in value.items()}

class JSONFilter(filters.Filter):
    field_class = JSONField

    def filter(self, qs, value):
        if value in EMPTY_VALUES:
            return qs
        return qs.filter(**value)

Ich habe das oben nicht getestet, aber es sollte ungefähr richtig sein. Es gibt jedoch Einschränkungen:

  • Soweit ich das beurteilen kann, gibt es keine Möglichkeit, pro-param ValidationError s korrekt zu behandeln
  • Schlechte OpenAPI/CoreAPI-Schemaunterstützung? Nicht sicher, wie das aussehen würde.
  • djangorestframework-filters ist nicht kompatibel mit MultiWidget . Dieser Filter/dieses Widget würde aus denselben Gründen auf dieselben Probleme stoßen.

@rpkilby vielen Dank für diese gründliche Antwort.

Wenn Ihr JSON-Feld einen "count"-Schlüssel hat, wäre es nicht möglich, zu erkennen, dass nur positive Zahlen gültig sind.

Das ist ein wichtiger Punkt, die Tatsache, dass wir den Typ des Abfragewerts nicht validieren können, macht es sehr schwierig, da MyModel.objects.filter(data__count="1") nicht dasselbe zurückgibt wie MyModel.objects.filter(data__count=1) . Wie Sie sagen, gibt es keine Möglichkeit, den Typ des Werts aus den Abfrageparametern zu erraten.

Daher bleibt nur die Option, die Typinformationen in den Abfragewert einzubetten, indem Sie etwas wie ?data__count=1:int tun, um nach Ganzzahlen und ?data__count=1:str nach Zeichenfolgen zu suchen, und so weiter. Aber wie es hier vorgeschlagen wird, wird dies nicht empfohlen.

Ich verstehe jetzt, warum es so wertvoll ist, die Filter explizit zu definieren. Trotzdem werde ich deinen Vorschlag ausprobieren! Danke noch einmal

@rpkilby , ich habe ein ähnliches Bedürfnis.

Ich habe eine Konfigurationstabelle wie diese mit zwei Spalten

meta_structure of type jsonb (This column has info like key1 of type string, key2 of type integer)

Ich habe eine andere Tabelle namens config_data, die 3 Spalten hat.

config_id -> Foreign key to config table
meta_info -> jsonb type

Hinweis: Die oben genannten Tabellen sind nicht die exakten Tabellen. Sie sind nur die repräsentativen Versionen, um die Botschaft zu vermitteln.

Ich validiere derzeit die Felder in der meta_info-Tabelle vor dem Speichern, indem ich die Übereinstimmung mit der Konfigurationstabelle überprüfe.

Die Notwendigkeit ist, dass ich mithilfe der meta_info-Spalte der config_data-Tabelle filtern möchte. Z.B. meta_info__key1='abc'. (key1 kann alles sein)

Ich habe versucht, den oben angegebenen Ansatz zu verwenden, aber das Problem ist, wie ich die JSONFilter-Klasse verwende, die Sie oben erstellt haben.

Z.B.

class ConfigDataFilterSet(django_filters.FilterSet):
    meta_info = JSONFilter(field_name='meta_info')

pp = ConfigDataFilterSet(data={'meta_info__key1': 'abc'})

Wenn ich jetzt pp.qs oder pp.filter_queryset() ausführe, wird der Filter nicht wirklich auf das Feld meta_info angewendet, da der in der Klasse ConfigDataFilterSet zugewiesene Feldname meta_info ist. Können Sie mir helfen, diese Hürde zu nehmen?

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen