Django-filter: Filtrer pour contrib.postgres JSONField

CrĂ©Ă© le 4 juin 2016  Â·  16Commentaires  Â·  Source: carltongibson/django-filter

Cela a commencé dans le groupe de discussion Google :
https://groups.google.com/forum/#!topic/django-filter/RwNfoWsdeLQ

Je souhaite pouvoir filtrer contrib.postgres JSONFields avec django-filter.

J'ai un filtre qui fonctionne pour quelques exemples. C'est plus compliquĂ© que je ne le pensais dans la mesure oĂč vous ne connaissez pas vraiment le type de donnĂ©es dans votre JSON Ă  l'avance comme vous le faites avec quelque chose comme un IntegerField. Je suis peut-ĂȘtre en train de compliquer les choses.

Voici un exemple de filtre qui frappe mon JSONField
http://127.0.0.1 :8000/api/v1/craters?data= latitude:float :-57:lte~!@!~ age:str :PC

Voici les modÚles et le code du filtre :
https://gist.github.com/jzmiller1/627071f555186cd1a58bb8f065205ff7

Je vais continuer à bosser dessus. Si quelqu'un a des idées ou des commentaires s'il vous plaßt faites le moi savoir...

Commentaire le plus utile

Je pense que ce serait vraiment incroyable d'avoir un JSONFilter permettant des requĂȘtes telles que jsonfield__a_random_key=value . Je sais que vous pouvez le faire avec la mĂ©thode objects.filter . Peut-ĂȘtre que le compromis pourrait ĂȘtre la validation du filtre ?

Tous les 16 commentaires

Bonjour @jzmiller1. Qu'essayez-vous exactement d'atteindre ? Je ne peux pas dire si vous ĂȘtes :

  • essayer de crĂ©er un JSONFilter gĂ©nĂ©rique qui vous permettra d'interroger n'importe quel attribut arbitraire Ă  l'intĂ©rieur d'un JSONField. ou,
  • essayant d'exposer des attributs spĂ©cifiques (latitude, Ăąge) qui sont communs Ă  votre cratĂšre data . Ces attributs seraient essentiellement le schĂ©ma de votre data .

Le premier est intéressant, mais comme vous l'avez découvert, la complication réside dans le fait que le JSONField est intrinsÚquement sans schéma. Sans schéma, vous ne pouvez pas écrire de code pour générer automatiquement des filtres. Votre MethodFilter fonctionne en ce sens qu'il autorise toute recherche d'attribut arbitraire, mais vous ne pouvez pas valider ces recherches. par exemple, ?data=latitude:char:PC:isnull est possible, mais absurde. Quelle que soit la solution ici, il faudra un compromis. Un filtre complÚtement arbitraire ne pourra pas valider les recherches, un filtre de validation nécessiterait un moyen de fournir un schéma.

Pour le deuxiĂšme cas, les solutions sont verbeuses / fastidieuses, mais simples.

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')
    ...

Votre requĂȘte ressemblerait alors à :

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

Mon objectif est de crĂ©er un JSONFilter gĂ©nĂ©rique qui permettra des requĂȘtes sur n'importe quel attribut arbitraire Ă  l'intĂ©rieur d'un JSONField. Pour ce sur quoi je travaille, je ne saurai pas vraiment ce qu'il y a dans les donnĂ©es d'un cratĂšre particulier, mais s'il y a une clĂ© lĂ -dedans que je recherche, j'aimerais pouvoir l'interroger.

En ce qui concerne l'incapacitĂ© de valider les types de recherche, je pense que je dĂ©pendrais de l'utilisateur qui effectue la requĂȘte pour se rendre compte que la requĂȘte est absurde et ne la fait tout simplement pas pour commencer.

Je ne sais pas si ce que j'essaie de faire est une perte de temps ou non. Il existe peut-ĂȘtre une meilleure solution pour ce que j'essaie d'accomplir. J'Ă©tais curieux de savoir si quelqu'un voyait des problĂšmes majeurs qui empĂȘcheraient cela d'ĂȘtre possible ou si quelqu'un avait un cas d'utilisation oĂč cela serait utile. Merci d'y avoir jetĂ© un Ɠil !

Ma premiÚre réflexion concerne la validation, selon le point de @rpkilby . L'absence de schéma est agréable du point de vue du développeur, mais je ne suis pas sûr que vous souhaitiez qu'il soit directement connecté aux URL adressables.

Laissons cela ouvert pour l'instant. Je vois que c'est une demande populaire. (Donc, mĂȘme l'aborder au niveau de _"Voici un exemple MethodFilter "_ dans la documentation en vaudrait la peine.)

Pour ce sur quoi je travaille, je ne saurai pas vraiment ce qu'il y a dans les données d'un cratÚre particulier, mais s'il y a une clé là-dedans que je recherche, j'aimerais pouvoir l'interroger.

Ça a l'air... un peu funky. Vous fournissez une API pour les donnĂ©es sur les cratĂšres, mais vous ne savez pas ce qu'il y a dans les donnĂ©es que vous fournissez ? Voulez-vous dire que certains enregistrements seront dĂ©pourvus des attributs d'un schĂ©ma commun, ou que les enregistrements individuels sont complĂštement arbitraires ?

Je vais fermer ceci comme hors de portée pour le moment. Heureux d'examiner les demandes d'extraction documentées et testées. Nous pourrions avoir la capacité de reconsidérer à l'avenir.

Je pense que ce serait vraiment incroyable d'avoir un JSONFilter permettant des requĂȘtes telles que jsonfield__a_random_key=value . Je sais que vous pouvez le faire avec la mĂ©thode objects.filter . Peut-ĂȘtre que le compromis pourrait ĂȘtre la validation du filtre ?

Je viens de terminer une implĂ©mentation d'une requĂȘte "naturelle" pour le filtre QuerySet Ă  l'aide de l'objet Q. Il a Ă©tĂ© testĂ© unitairement par rapport Ă  un ensemble de requĂȘtes avec ~ 1000 enregistrements Ă  l'aide d'un JsonField. La mise en Ɠuvre est à :
https://github.com/shallquist/DJangoQuerySetFilter/blob/master/queryparser.py

Hé @shallquist, je ne sais pas comment utiliser QuerySetFilter dans le contexte de django-filter. Avez-vous documenté l'utilisation quelque part?

C'est assez simple Ă  utiliser comme le montre le readme sur github. Les requĂȘtes normales doivent ĂȘtre prises en charge, c'est-Ă -dire.
QuerySetFilter('friends').get_Query((person__address__city = Denver | person__address__city = Boulder) & person__address_state ~= CO)
qui construira une requĂȘte pour rĂ©cupĂ©rer tous les amis qui vivent Ă  Dever ou Boulder colorado, oĂč friends est un jsonfield.

BTW Cela n'a pas été beaucoup testé et comme les filtres Django ne prennent pas en charge l'interrogation des objets de tableau intégrés, j'ai abandonné cette approche.

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

Je pense que ce serait vraiment incroyable d'avoir un JSONFilter permettant des requĂȘtes telles que jsonfield__a_random_key=value. Je sais que vous pouvez le faire avec la mĂ©thode objects.filter. Peut-ĂȘtre que le compromis pourrait ĂȘtre la validation du filtre ?

HĂ© @carltongibson , @rpkilby , j'aimerais avoir votre avis Ă  ce sujet. Disons que my_field est un postgres JSONField , et je veux :

  • Ajoutez un filtre REST sous la forme de my_field__etc=value oĂč etc est l'une des requĂȘtes prises en charge par JSONField et value tout ce que l'utilisateur REST fournit.
  • Ensuite, je voudrais passer etc et value au gestionnaire d'objets modĂšles sous la forme de MyModel.objects.filter(my_field__etc=value) .
  • Enfin, rĂ©cupĂ©rez tout ce que le filtre renvoie.

Cela semble super trivial mais je n'ai pas compris comment faire quelque chose comme ça. Si vous me donnez un petit indice, je pourrais essayer de l'implémenter.

Toutes les pensées seraient super appréciées!

Est-ce que quelque chose comme ce qui suit ne fonctionne pas ?

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

En gĂ©nĂ©ral, le field_name doit correspondre au nom du champ de modĂšle sous-jacent, tandis que les transformations et les recherches (une transformation clĂ© dans ce cas) doivent ĂȘtre contenues dans le lookup_expr .

@rpkilby merci beaucoup pour une rĂ©ponse aussi rapide - Ouais exactement, mais je veux que etc fourni par l'utilisateur dans la requĂȘte... Donc je ne pouvais pas vraiment le coder en dur dans le filtre 💭

Le filtre devrait plutÎt ressembler à :

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

Ainsi, un seul filtre JSON pour gĂ©rer des paramĂštres de requĂȘte arbitraires comme ?my_field__etc=value .

Je vois deux problĂšmes. PremiĂšrement, une partie de la valeur de django-filter est qu'il valide les paramĂštres de la requĂȘte. Étant donnĂ© que JSONField n'ont pas de schĂ©ma, il n'est pas possible de gĂ©nĂ©rer des filtres qui valident de maniĂšre appropriĂ©e les donnĂ©es entrantes. par exemple, si votre champ JSON a une clĂ© "count", il ne serait pas possible de deviner que seuls les nombres positifs sont valides. Le mieux que l'on puisse faire est de garantir que la valeur est valide JSON. Ainsi, les requĂȘtes seraient au moins valides, mais peut-ĂȘtre absurdes (par exemple, data__count__gt='cat' ).

La seconde est que ce filtre va avoir les mĂȘmes limitations que les filtres basĂ©s sur MultiWidget . par exemple, il ne gĂ©nĂ©rera pas d'erreurs de validation pour les noms de paramĂštres corrects. Mais avant de plonger lĂ -dedans, voici comment j'implĂ©menterais probablement le filtre. Nous avons besoin:

  • Une classe de filtre pour effectuer le filtrage proprement dit, qui doit gĂ©rer plusieurs paramĂštres
  • Un champ de formulaire pour valider les donnĂ©es JSON
  • Un widget pour obtenir les donnĂ©es des paramĂštres arbitraires my_field__* .
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)

Je n'ai pas testĂ© ce qui prĂ©cĂšde, mais cela devrait ĂȘtre Ă  peu prĂšs correct. Cependant, il existe des limites :

  • Autant que je sache, il n'y a aucun moyen de gĂ©rer correctement par paramĂštre ValidationError s
  • Mauvaise prise en charge du schĂ©ma OpenAPI/CoreAPI ? Je ne sais pas Ă  quoi cela ressemblerait.
  • djangorestframework-filters n'est pas compatible avec MultiWidget . Ce filtre/widget rencontrerait les mĂȘmes problĂšmes pour les mĂȘmes raisons.

@rpkilby merci beaucoup pour cette réponse complÚte.

si votre champ JSON a une clé "count", il ne serait pas possible de deviner que seuls les nombres positifs sont valides.

C'est un excellent point, le fait que nous ne puissions pas valider le type de valeur de la requĂȘte le rend trĂšs difficile car MyModel.objects.filter(data__count="1") ne renverra pas la mĂȘme chose que MyModel.objects.filter(data__count=1) . Comme vous le dites, il n'y a aucun moyen de deviner le type de la valeur Ă  partir des paramĂštres de la requĂȘte.

Ne laissant donc que l'option d'intĂ©grer les informations de type dans la valeur de la requĂȘte, en faisant quelque chose comme ?data__count=1:int pour rechercher un entier et ?data__count=1:str pour les chaĂźnes, etc. Mais comme il est suggĂ©rĂ© ici , ce n'est pas recommandĂ©.

Je comprends maintenant pourquoi il est si précieux de définir explicitement les filtres. Néanmoins, je vais essayer votre suggestion! Merci encore

@rpkilby , j'ai un besoin similaire.

J'ai une table de configuration comme celle-ci avec deux colonnes

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

J'ai une autre table nommée config_data qui aura 3 colonnes.

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

Remarque : Les tableaux mentionnés ci-dessus ne sont pas les tableaux exacts. Ce ne sont que des versions représentatives pour transmettre le message.

Je valide actuellement les champs de la table meta_info avant de les enregistrer en vérifiant qu'ils correspondent à la table de configuration.

Le besoin est que je veux filtrer en utilisant la colonne meta_info de la table config_data. Par exemple. meta_info__key1='abc'. (key1 peut ĂȘtre n'importe quoi)

J'essayais d'utiliser l'approche que vous aviez donnée ci-dessus, mais le problÚme est de savoir comment utiliser la classe JSONFilter que vous avez créée ci-dessus.

Par exemple.

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

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

Maintenant, si j'exécute pp.qs ou pp.filter_queryset() , il n'appliquera pas réellement le filtre sur le champ meta_info car le nom de champ attribué dans la classe ConfigDataFilterSet est meta_info. Pouvez-vous m'aider à surmonter cet obstacle?

Cette page vous a été utile?
0 / 5 - 0 notes