Django-filter: Фильтр для contrib.postgres JSONField

Созданный на 4 июн. 2016  ·  16Комментарии  ·  Источник: carltongibson/django-filter

Это началось в группе обсуждения Google:
https://groups.google.com/forum/#!topic/django-filter/RwNfoWsdeLQ

Меня интересует возможность фильтрации contrib.postgres JSONFields с помощью django-filter.

У меня есть фильтр, который работает для нескольких примеров. Это сложнее, чем я думал, поскольку вы действительно не знаете заранее тип данных в своем JSON, как вы это делаете с чем-то вроде IntegerField. Может быть, я просто слишком усложняю.

Вот пример фильтра, который попадает в мой JSONField
http://127.0.0.1 :8000/api/v1/craters?data= latitude:float :-57:lte~!@!~ age:str :PC

Вот модели и код фильтра:
https://gist.github.com/jzmiller1/627071f555186cd1a58bb8f065205ff7

Я собираюсь продолжать возиться с этим. Если у кого-то есть какие-либо мысли или отзывы, пожалуйста, дайте мне знать...

Самый полезный комментарий

Я думаю, было бы здорово иметь JSONFilter, поддерживающий такие запросы, как jsonfield__a_random_key=value . Я знаю, что вы можете сделать это с помощью метода objects.filter . Возможно, компромиссом может быть проверка фильтра?

Все 16 Комментарий

Привет @jzmiller1. Чего именно вы пытаетесь достичь? Не могу сказать, являетесь ли вы:

  • пытаясь создать общий JSONFilter, который позволит вам запрашивать любой произвольный атрибут внутри JSONField. или,
  • пытаясь выявить определенные атрибуты (широта, возраст), которые являются общими для вашего кратера data . Эти атрибуты, по сути, будут вашей схемой data .

Первое интересно, но, как вы узнали, сложность заключается в том, что JSONField по своей сути не имеет схемы. Без схемы вы не сможете написать код для автоматического создания фильтров. Ваш MethodFilter работает в том смысле, что он разрешает любой произвольный поиск атрибутов, но вы не можете проверить эти поиски. например, ?data=latitude:char:PC:isnull возможно, но бессмысленно. Любое решение здесь потребует компромисса. Совершенно произвольный фильтр не сможет проверить поиск, для проверки фильтра потребуется какой-то способ предоставления схемы.

Во втором случае решения многословны/утомительны, но просты.

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

Тогда ваш запрос будет выглядеть так:

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

Моя цель — создать общий JSONFilter, который позволит выполнять запросы к любому произвольному атрибуту внутри JSONField. Что касается того, над чем я работаю, я действительно не знаю, что находится внутри данных для конкретного кратера, но если там есть ключ, который я ищу, я хотел бы иметь возможность запросить его.

Что касается невозможности проверки типов поиска, я думаю, что я буду зависеть от пользователя, делающего запрос, чтобы понять, что запрос бессмысленный, и просто не делать его с самого начала.

Я не уверен, что то, что я пытаюсь сделать, это пустая трата времени или нет. Там может быть лучшее решение для того, что я пытаюсь выполнить. Мне было любопытно, видел ли кто-нибудь серьезные проблемы, из-за которых это было бы невозможно, или был ли у кого-нибудь вариант использования, в котором это было бы полезно. Спасибо, что заглянули!

Моя первая мысль о проверке, согласно точке зрения @rpkilby . Отсутствие схемы — это хорошо с точки зрения разработчика, но я не уверен, что вы хотите, чтобы она была связана напрямую с адресуемыми URL-адресами.

Давайте пока оставим это открытым. Я вижу, что это популярный запрос. (Таким образом, было бы целесообразно обратиться к нему на уровне _"Вот пример MethodFilter "_ в документации.)

Что касается того, над чем я работаю, я действительно не знаю, что находится внутри данных для конкретного кратера, но если там есть ключ, который я ищу, я хотел бы иметь возможность запросить его.

Это кажется... немного забавным. Вы предоставляете API для данных о кратерах, но не знаете, что содержится в данных, которые вы предоставляете? Вы имеете в виду, что в некоторых записях будут отсутствовать атрибуты общей схемы или что отдельные записи будут совершенно произвольными?

Я собираюсь закрыть это как вне области на данный момент. С удовольствием рассмотрю задокументированные, проверенные запросы на вытягивание. У нас может быть возможность пересмотреть свое решение в будущем.

Я думаю, было бы здорово иметь JSONFilter, поддерживающий такие запросы, как jsonfield__a_random_key=value . Я знаю, что вы можете сделать это с помощью метода objects.filter . Возможно, компромиссом может быть проверка фильтра?

Только что завершил реализацию «естественного» запроса для фильтра QuerySet с использованием объекта Q. Он был протестирован на наборе запросов с ~ 1000 записей с использованием JsonField. Реализация находится по адресу: г.
https://github.com/shallquist/DJangoQuerySetFilter/blob/master/queryparser.py

Привет , @shallquist , я не знаю, как использовать QuerySetFilter в контексте django-filter. Вы где-то документировали использование?

Его довольно просто использовать, как показано в файле readme на github. Обычные запросы должны поддерживаться, т.е.
QuerySetFilter('друзья').get_Query((person__address__city = Denver | person__address__city = Boulder) & person__address_state ~= CO)
который создаст запрос для получения всех друзей, которые живут в Девере или Боулдере, штат Колорадо, где друзья — это jsonfield.

Кстати, это мало тестировалось, и, поскольку фильтры Django не поддерживают запросы к встроенным объектам массива, я отказался от этого подхода.

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

Я думаю, было бы здорово иметь JSONFilter, поддерживающий такие запросы, как jsonfield__a_random_key=value. Я знаю, что вы можете сделать это с помощью метода objects.filter. Возможно, компромиссом может быть проверка фильтра?

Привет, @carltongibson , @rpkilby , я хотел бы узнать ваше мнение по этому поводу. Допустим, my_field — это postgres JSONField , и я хочу:

  • Добавьте фильтр REST в форме my_field__etc=value , где etc — любой из запросов, поддерживаемых JSONField и value независимо от того, что предоставляет пользователь REST.
  • Затем я хотел бы передать etc и value менеджеру объектов модели в виде MyModel.objects.filter(my_field__etc=value) .
  • Наконец, извлеките все, что возвращает фильтр.

Это кажется супер тривиальным, но я не придумал, чтобы сделать что-то подобное. Если вы, ребята, дадите мне немного подсказки, я мог бы попытаться реализовать это.

Любые мысли будут супер оценены!

Что-то вроде следующего не работает?

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

Как правило, field_name должен соответствовать имени поля базовой модели, а преобразования и поиски (в данном случае ключевое преобразование) должны содержаться в lookup_expr .

@rpkilby большое спасибо за такой быстрый ответ. Да, именно так, но я хочу, чтобы etc было предоставлено пользователем в запросе... Так что я не мог жестко закодировать это в фильтре 💭

Фильтр должен выглядеть примерно так:

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

Итак, один фильтр JSON для обработки произвольных параметров запроса, таких как ?my_field__etc=value .

Я вижу две проблемы. Во-первых, часть ценности django-filter заключается в том, что он проверяет параметры запроса. Поскольку у JSONField нет схемы, невозможно создать фильтры, которые должным образом проверяют входящие данные. например, если ваше поле JSON имеет ключ «количество», было бы невозможно интуитивно понять, что допустимы только положительные числа. Лучшее, что можно сделать, это гарантировать, что значение является допустимым JSON. Таким образом, запросы будут, по крайней мере, действительными, но, возможно, бессмысленными (например, data__count__gt='cat' ).

Во-вторых, этот фильтр будет иметь те же ограничения, что и фильтры на основе MultiWidget . например, он не будет генерировать ошибки проверки для правильных имен параметров. Но прежде чем углубиться в это, вот как я, вероятно, реализую фильтр. Нам нужно:

  • Класс фильтра для выполнения фактической фильтрации, которая должна обрабатывать несколько параметров.
  • Поле формы для проверки данных JSON
  • Виджет для получения данных для произвольных параметров 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)

Я не проверял вышеизложенное, но это должно быть примерно правильно. Однако есть ограничения:

  • Насколько я могу судить, нет способа правильно обрабатывать ValidationError для каждого параметра.
  • Плохая поддержка схемы OpenAPI/CoreAPI? Не уверен, как это будет выглядеть.
  • djangorestframework-filters несовместим с MultiWidget . Этот фильтр/виджет будет сталкиваться с теми же проблемами по тем же причинам.

@rpkilby большое спасибо за этот подробный ответ.

если в вашем поле JSON есть ключ «счетчик», было бы невозможно интуитивно понять, что допустимы только положительные числа.

Это отличный момент, тот факт, что мы не можем проверить тип значения запроса, делает его очень сложным, поскольку MyModel.objects.filter(data__count="1") не вернет то же самое, что и MyModel.objects.filter(data__count=1) . Как вы говорите, невозможно угадать тип значения из параметров запроса.

Следовательно, остается только возможность встроить информацию о типе в значение запроса, выполнив что-то вроде ?data__count=1:int для поиска целых чисел и ?data__count=1:str для строк и так далее. Но, как это предлагается здесь , это не рекомендуется.

Теперь я понимаю, почему так важно явно определять фильтры. Тем не менее, я попробую ваш совет! Еще раз спасибо

@rpkilby , у меня похожая потребность.

У меня есть такая таблица конфигурации с двумя столбцами

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

У меня есть другая таблица с именем config_data, которая будет иметь 3 столбца.

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

Примечание . Таблицы, упомянутые выше, не являются точными таблицами. Это просто репрезентативные версии для передачи сообщения.

В настоящее время я проверяю поля в таблице meta_info перед сохранением, проверяя их соответствие из таблицы конфигурации.

Необходимость в том, что я хочу фильтровать, используя столбец meta_info таблицы config_data. Например. meta_info__key1='abc'. (key1 может быть любым)

Я пытался использовать подход, который вы указали выше, но проблема в том, как мне использовать класс JSONFilter, который вы создали выше.

Например.

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

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

Теперь, если я запущу pp.qs или pp.filter_queryset() , он фактически не применит фильтр к полю meta_info, потому что имя поля, присвоенное в классе ConfigDataFilterSet, — meta_info. Можете ли вы помочь мне преодолеть это препятствие?

Была ли эта страница полезной?
0 / 5 - 0 рейтинги