Django-filter: Filtrar para contrib.postgres JSONField

Criado em 4 jun. 2016  ·  16Comentários  ·  Fonte: carltongibson/django-filter

Isso começou no grupo de discussão do Google:
https://groups.google.com/forum/#!topic/django -filter/RwNfoWsdeLQ

Estou interessado em poder filtrar contrib.postgres JSONFields com django-filter.

Eu tenho um filtro que está funcionando para alguns exemplos. Isso é mais complicado do que eu pensava, pois você realmente não conhece o tipo de dados em seu JSON com antecedência da maneira que faz com algo como um IntegerField. Eu só posso estar complicando demais.

Aqui está um filtro de exemplo que atinge meu JSONField
http://127.0.0.1 :8000/api/v1/craters?data= latitude:float :-57:lte~!@!~ age:str :PC

Aqui estão os modelos e o código do filtro:
https://gist.github.com/jzmiller1/627071f555186cd1a58bb8f065205ff7

Vou continuar brincando com isso. Se alguém tiver alguma opinião ou feedback, por favor me avise...

Comentários muito úteis

Eu acho que seria realmente incrível ter um JSONFilter habilitando consultas como jsonfield__a_random_key=value . Eu sei que você pode fazer isso com o método objects.filter . Talvez a compensação possa ser a validação do filtro?

Todos 16 comentários

Olá @jzmiller1. O que exatamente você está tentando alcançar? Não sei dizer se você é:

  • tentando criar um JSONFilter genérico que permitirá consultar qualquer atributo arbitrário dentro de um JSONField. ou,
  • tentando expor atributos específicos (latitude, idade) que são comuns à sua cratera data . Esses atributos seriam essencialmente o esquema do seu data .

O primeiro é interessante, mas, como você descobriu, a complicação está no fato de que o JSONField é inerentemente sem esquema. Sem um esquema, você não pode escrever código para gerar filtros automaticamente. Seu MethodFilter funciona para permitir qualquer pesquisa de atributo arbitrário, mas você não consegue validar essas pesquisas. por exemplo, ?data=latitude:char:PC:isnull é possível, mas sem sentido. Qualquer solução aqui vai exigir uma troca. Um filtro completamente arbitrário não poderá validar as pesquisas, um filtro de validação exigiria alguma forma de fornecer um esquema.

Para o segundo caso, as soluções são detalhadas/tediosas, mas diretas.

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

Sua consulta ficaria então:

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

Meu objetivo é criar um JSONFilter genérico que permita consultas em qualquer atributo arbitrário dentro de um JSONField. Para o que estou trabalhando, não saberei realmente o que está dentro dos dados de uma cratera em particular, mas se houver uma chave lá que estou procurando, gostaria de poder consultá-la.

No que diz respeito à incapacidade de validar os tipos de pesquisa, acho que dependeria do usuário que faz a consulta para perceber que a consulta não faz sentido e simplesmente não a faz para começar.

Não tenho certeza se o que estou tentando fazer é uma perda de tempo ou não. Pode haver uma solução melhor para o que estou tentando realizar. Fiquei curioso se alguém viu problemas importantes que impediriam que isso fosse possível ou se alguém tivesse um caso de uso em que isso seria útil. Obrigado por dar uma olhada!

Meu primeiro pensamento é sobre a validação, conforme o ponto de @rpkilby . Schema-less é bom do ponto de vista do desenvolvedor - mas não tenho certeza se você deseja que ele seja conectado diretamente a URLs endereçáveis.

Vamos manter isso aberto por enquanto. Eu posso ver que é um pedido popular. (Então, mesmo para abordá-lo no nível de _"Aqui está um exemplo MethodFilter "_ nos documentos valeria a pena.)

Para o que estou trabalhando, não saberei realmente o que está dentro dos dados de uma cratera em particular, mas se houver uma chave lá que estou procurando, gostaria de poder consultá-la.

Isso parece... meio esquisito. Você está fornecendo uma API para dados de crateras, mas não sabe o que há nos dados que está fornecendo? Você quer dizer que alguns registros terão atributos ausentes de um esquema comum ou que registros individuais são completamente arbitrários?

Vou fechar isso como fora do escopo no momento. Feliz em considerar solicitações de pull documentadas e testadas. Podemos ter capacidade para reconsiderar no futuro.

Eu acho que seria realmente incrível ter um JSONFilter habilitando consultas como jsonfield__a_random_key=value . Eu sei que você pode fazer isso com o método objects.filter . Talvez a compensação possa ser a validação do filtro?

Acabei de concluir uma implementação de consulta 'natural' para o filtro QuerySet usando o objeto Q. Ele foi testado por unidade em um conjunto de consultas com ~ 1.000 registros usando um JsonField. A implementação está em:
https://github.com/shallquist/DJangoQuerySetFilter/blob/master/queryparser.py

Ei @shallquist, não tenho certeza de como usar QuerySetFilter no contexto do django-filter. Você documentou o uso em algum lugar?

É bastante simples de usar, como é mostrado no leia-me no github. As consultas normais devem ser suportadas, ou seja.
QuerySetFilter('friends').get_Query((person__address__city = Denver | person__address__city = Boulder) & person__address_state ~= CO)
que irá construir uma consulta para recuperar todos os amigos que moram em Dever ou Boulder colorado, onde friends é um jsonfield.

BTW Isso não foi muito testado e como os filtros do Django não suportam a consulta de objetos de matriz incorporados, abandonei essa abordagem.

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

Eu acho que seria realmente incrível ter um JSONFilter habilitando consultas como jsonfield__a_random_key=value. Eu sei que você pode fazer isso com o método objects.filter. Talvez a compensação possa ser a validação do filtro?

Ei @carltongibson , @rpkilby eu gostaria de saber sua opinião sobre isso. Digamos que my_field seja um postgres JSONField e eu queira:

  • Adicione um filtro REST na forma de my_field__etc=value onde etc é qualquer uma das consultas suportadas por JSONField e value o que o usuário REST fornecer.
  • Então eu gostaria de passar etc e value para o gerenciador de objetos do modelo na forma de MyModel.objects.filter(my_field__etc=value) .
  • Por fim, recupere o que o filtro retornar.

Parece super trivial, mas eu não descobri como fazer algo assim. Se vocês me derem uma pista, posso tentar implementar.

Quaisquer pensamentos seriam super apreciados!

Algo como o seguinte não funciona?

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

Em geral, o field_name deve corresponder ao nome do campo do modelo subjacente, enquanto as transformações e pesquisas (uma transformação de chave nesse caso) devem estar contidas no lookup_expr .

@rpkilby muito obrigado por uma resposta tão rápida - Sim, exatamente, mas eu quero que etc seja fornecido pelo usuário na solicitação ... Então eu não consegui codificar no filtro 💭

O filtro deve ficar mais parecido com:

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

Portanto, um único filtro JSON para lidar com parâmetros de consulta arbitrários como ?my_field__etc=value .

Eu vejo dois problemas. Primeiro, parte do valor de django-filter é que ele valida os parâmetros de consulta. Como JSONField s não têm esquema, não é possível gerar filtros que validem adequadamente os dados de entrada. por exemplo, se o seu campo JSON tiver uma chave "count", não seria possível intuir que apenas números positivos são válidos. O melhor que pode ser feito é garantir que o valor seja JSON válido. Portanto, as consultas seriam pelo menos válidas, mas possivelmente sem sentido (por exemplo, data__count__gt='cat' ).

A segunda é que este filtro terá as mesmas limitações dos filtros baseados em MultiWidget . por exemplo, não irá gerar erros de validação para os nomes de parâmetros corretos. Mas antes de mergulhar nisso, veja como eu provavelmente implementaria o filtro. Nós precisamos:

  • Uma classe de filtro para realizar a filtragem real, que deve lidar com vários parâmetros
  • Um campo de formulário para validar os dados JSON
  • Um widget para obter os dados para os parâmetros my_field__* arbitrários.
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)

Eu não testei o acima, mas deve estar aproximadamente correto. No entanto, existem limitações:

  • Tanto quanto eu posso dizer, não há como lidar corretamente com ValidationError s por parâmetro
  • Suporte deficiente ao esquema OpenAPI/CoreAPI? Não tenho certeza de como isso seria.
  • djangorestframework-filters não é compatível com MultiWidget . Esse filtro/widget teria os mesmos problemas pelos mesmos motivos.

@rpkilby muito obrigado por esta resposta completa.

se o seu campo JSON tiver uma chave "count", não seria possível intuir que apenas números positivos são válidos.

Este é um ótimo ponto, o fato de não podermos validar o tipo de valor da consulta o torna muito desafiador, pois MyModel.objects.filter(data__count="1") não retornará o mesmo que MyModel.objects.filter(data__count=1) . Como você disse, não há como adivinhar o tipo do valor dos parâmetros de consulta.

Portanto, deixando apenas a opção de incorporar as informações do tipo no valor da consulta, fazendo algo como ?data__count=1:int para procurar por inteiro e ?data__count=1:str por strings e assim por diante. Mas, como sugerido aqui , isso não é recomendado.

Agora entendo por que é tão valioso definir explicitamente os filtros. No entanto, vou tentar a sua sugestão! obrigado novamente

@rpkilby , tenho uma necessidade semelhante.

Eu tenho uma tabela de configuração como esta com duas colunas

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

Eu tenho outra tabela chamada config_data que terá 3 colunas.

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

Nota: As tabelas mencionadas acima não são as tabelas exatas. São apenas as versões representativas para transmitir a mensagem.

Atualmente, valido os campos na tabela meta_info antes de salvar, verificando sua correspondência na tabela de configuração.

A necessidade é que eu queira filtrar usando a coluna meta_info da tabela config_data. Por exemplo. meta_info__key1='abc'. (key1 pode ser qualquer coisa)

Eu estava tentando usar a abordagem que você deu acima, mas o problema é como uso a classe JSONFilter que você criou acima.

Por exemplo.

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

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

Agora, se eu executar pp.qs ou pp.filter_queryset() , ele não aplicará o filtro no campo meta_info porque o nome do campo atribuído na classe ConfigDataFilterSet é meta_info. Você pode me ajudar a superar esse obstáculo?

Esta página foi útil?
0 / 5 - 0 avaliações