Как создать общие механизмы фильтрации в строке запроса API?

Я создаю общий API с контентом и схему, которая может быть определена пользователем. Я хочу добавить логику фильтрации к ответам API, чтобы пользователи могли запрашивать конкретные объекты, которые они хранили в API. Например, если пользователь хранит объекты событий, они могут делать такие вещи, как фильтр:

  • Массив содержит: properties.categories содержит Engineering
  • Большая, чем: properties.created_at старше 2016-10-02
  • Не равно: properties.address.city не Washington
  • Равно: properties.name Meetup
  • и др.

Я пытаюсь создать фильтрацию в строке запроса ответов API и придумать несколько вариантов, но я не уверен, какой синтаксис для нее лучше...


1. Оператор как вложенный ключ

/events?properties.name=Harry&properties.address.city.neq=Washington

В этом примере используется только вложенный объект для конкретных операторов (например, neq, как показано). Это приятно, потому что это очень просто и легко читается.

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

Пример: Stripe API


2. Оператор в качестве ключевого суффикса

/events?properties.name=Harry&properties.address.city+neq=Washington

Этот пример похож на первый, за исключением того, что вместо . используется разделитель + (который эквивалентен пробелу), а не ., так что нет путаницы, поскольку ключи в моем домене могут " t содержит пробелы.

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


3. Оператор как префикс значения

/events?properties.name=Harry&properties.address.city=neq:Washington

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

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

Пример: API Sparkpay


4. Пользовательский параметр фильтра

/events?filter=properties.name==Harry;properties.address.city!=Washington

В этом примере используется один параметр запроса верхнего уровня, filter, чтобы пространство имен помещало всю логику фильтрации в. Это приятно в том, что вам никогда не придется беспокоиться о столкновении пространства имен верхнего уровня. (Хотя в моем случае все обычаи вложены под properties., поэтому это не проблема в первую очередь.)

Но это происходит из-за того, что требуется сложная строка запроса, когда вы хотите сделать базовую фильтрацию равенства, что, вероятно, приведет к необходимости часто проверять документацию. И использование символов для операторов может привести к путанице для неочевидных операций, таких как "рядом" или "внутри" или "содержит".

Пример: API Google Analytics


5. Пользовательский параметр подробного фильтра

/events?filter=properties.name eq Harry; properties.address.city neq Washington

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

Но это происходит за счет наличия более длинного URL-адреса и множества пробелов, которые необходимо закодировать?

Пример: API OData


6. Параметры фильтра объектов

/events?filter[1][key]=properties.name&filter[1][eq]=Harry&filter[2][key]=properties.address.city&filter[2][neq]=Washington

В этом примере также используется параметр верхнего уровня filter, но вместо создания полностью настраиваемого синтаксиса для него, который имитирует программирование, вместо этого создается определение объектов для фильтров с использованием более стандартного синтаксиса строки запроса. Это может принести немного больше "стандартных".

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

Пример Magento API


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

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

Ответ 1

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

Во-первых, вы говорите о "общем API с контентом и схеме, которая может быть определена пользователем".

Это очень похоже на solr/elasticsearch, которые являются общими для hi-уровня поверх Apache Lucene, который в основном индексирует и агрегирует документы.

Эти двое взяли совершенно разные подходы к их API для отдыха, мне пришлось работать с ними обоими.

Elasticsearch:

Они сделали весь запрос на DSL на основе JSON, который в настоящее время выглядит следующим образом:

GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }}, 
        { "match": { "content": "Elasticsearch" }}  
      ],
      "filter": [ 
        { "term":  { "status": "published" }}, 
        { "range": { "publish_date": { "gte": "2015-01-01" }}} 
      ]
    }
  }
}

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

Из моего личного опыта эта DSL была мощной, но довольно трудной для обучения и использования свободно (особенно в старых версиях). И чтобы получить какой-то результат, вам нужно больше, чем просто играть с URL-адресом. Исходя из того, что многие клиенты даже не поддерживают данные в запросе GET.

SOLR:

Они помещают все в параметры запроса, которые в основном выглядят так (взятые из doc):

q=*:*&fq={!cache=false cost=5}inStock:true&fq={!frange l=1 u=4 cache=false cost=50}sqrt(popularity)

Работа с этим была более простой. Но это только мой личный вкус.


Теперь о моем опыте. Мы реализовали еще один слой над этими двумя, и мы взяли номер подхода # 4. На самом деле, я думаю, что # 4 и # 5 должны поддерживаться в в то же время. Зачем? Поскольку все, что вы выбираете, люди будут жаловаться, и так как у вас будет свой собственный "микро-DSL", вы также можете поддерживать несколько псевдонимов для ваших ключевых слов.

Почему бы не # 2? Наличие одного параметра фильтра и запроса внутри дает вам полный контроль над DSL. Через полгода после того, как мы создали наш ресурс, мы получили "простой" запрос функции - логический OR и скобку (). Параметры запроса в основном представляют собой список операций AND, а логический OR как city=London OR age>25 на самом деле не подходит. С другой стороны, в скобках введена вставка в структуру DSL, что также будет проблемой в строковой структуре строки запроса.

Ну, это те проблемы, на которые мы наткнулись, ваше дело может быть другим. Но по-прежнему стоит учитывать, какие будущие ожидания от этого API будут.

Ответ 2

# 4

Мне нравится, как Google Analytics API-фильтр выглядит, использовать и легко понять с клиентской точки зрения.

Они используют кодированную форму URL, например:

  • Равно: % 3D% 3D filters=ga:timeOnPage%3D%3D10
  • Не равно: !% 3D filters=ga:timeOnPage!%3D10

Хотя вам нужно проверить документацию, но у нее все еще есть свои преимущества. ЕСЛИ вы думаете, что пользователи могут привыкнуть к этому, а затем пойти на это.


# 2

Использование операторов в качестве основных суффиксов также кажется хорошей идеей (согласно вашим требованиям).

Однако я бы рекомендовал кодировать знак +, чтобы он не анализировался как space. Также может быть немного сложнее разобрать, как упоминалось, но я думаю, что вы можете написать собственный парсер для этого. Я наткнулся на этот из jlong some время назад. Возможно, вам будет полезно написать парсер.

Ответ 3

Вы также можете попробовать Spring Язык выражений (SpEL)

Все, что вам нужно сделать, это придерживаться указанного формата в документе, движок Spel позаботится о разборе запроса и выполнении его на заданном объекте. Подобно вашему требованию о фильтрации списка объектов, вы можете написать запрос как:

properties.address.city == 'Washington' and properties.name == 'Harry'

Он поддерживает все виды реляционных и логических операторов, которые вам понадобятся. Остальная часть api может просто взять этот запрос в качестве строки фильтра и передать его движку Spel для работы на объекте.

Преимущества: он легко читается, легко записывается, и выполнение хорошо заботится.

Итак, URL-адрес будет выглядеть так:

/events?filter="properties.address.city == 'Washington' and properties.name == 'Harry'"

Пример кода с использованием org.springframework: spring -core: 4.3.4.RELEASE:

Основная интересующая функция:

    /**
     * Filter the list of objects based on the given query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

Полный пример с вспомогательными классами и другим неинтересным кодом:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class SpELTest {

    public static void main(String[] args) {
        String query = "address.city == 'Washington' and name == 'Harry'";

        Event event1 = new Event(new Address("Washington"), "Harry");
        Event event2 = new Event(new Address("XYZ"), "Harry");

        List<Event> events = Arrays.asList(event1, event2);

        List<Event> filteredEvents = filter(query, events);

        System.out.println(filteredEvents.size()); // 1
    }

    /**
     * Filter the list of objects based on the query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

    public static class Event {
        private Address address;
        private String name;

        public Event(Address address, String name) {
            this.address = address;
            this.name = name;
        }

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

    }

    public static class Address {
        private String city;

        public Address(String city) {
            this.city = city;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }

    }
}

Ответ 4

Я знаю, что это старая школа, но как насчет какой-то перегрузки оператора?

Это сделало бы запрос синтаксическим анализом намного сложнее (а не стандартным CGI), но будет похож на содержимое предложения SQL WHERE.

/events?properties.name=Harry&properties.address.city+neq=Washington

станет

/events?properties.name == 'Гарри' & &! Properties.address.city = 'Вашингтон' || properties.name == 'Jack' & &! Properties.address.city = ( "Париж", "Новый Орлеан" )

paranthesis начнет список. Сохранение строк в кавычках упростило бы синтаксический анализ.

Таким образом, вышеупомянутый запрос будет для событий для Гарри не в Вашингтоне или для Джеков, не в Париже или в Новом Орлеане.

Было бы тонкой работы по внедрению... и оптимизация базы данных для запуска этих запросов была бы кошмаром, но если вы ищете простой и мощный язык запросов, просто подражайте SQL:)

-k

Ответ 5

Я решил сравнить подходы № 1/№ 2 (1) и № 3 (2) и пришел к выводу, что (1) предпочтительнее (по крайней мере, для стороны Java-сервера).

Предположим, что некоторый параметр a должен быть равен 10 или 20. Наш URL-запрос в этом случае должен выглядеть так ?a.eq=10&a.eq=20 for (1) и ?a=eq:10&a=eq:20 ?a.eq=10&a.eq=20 ?a=eq:10&a=eq:20 for (2), В Java HttpServletRequest#getParameterMap() вернет следующие значения: { a.eq: [10, 20] } для (1) и { a: [eq:10, eq:20] } для (2). Позже мы должны преобразовать возвращенные карты, например, в SQL, where предложение. И мы должны получить: where a = 10 or a = 20 для обоих (1) и (2). Вкратце это выглядит примерно так:

1) ?a=eq:10&a=eq:20 -> { a: [eq:10, eq:20] } -> where a = 10 or a = 20
2) ?a.eq=10&a.eq=20 -> { a.eq: [10, 20] }    -> where a = 10 or a = 20

Итак, мы получили следующее правило: когда мы проходим через URL-запрос два параметра с одинаковым именем, мы должны использовать операнд OR в SQL.

Но давайте предположим другой случай. Параметр a должен быть больше 10 и меньше 20. Применяя указанное выше правило, мы получим следующее преобразование:

1) ?a.gt=10&a.ls=20 -> { a.gt: 10, a.lt: 20 } -> where a > 10 and a < 20
2) ?a=gt:10&a=ls:20 -> { a: [gt.10, lt.20] }  -> where a > 10 or(?!) a < 20

Как видите, в (1) у нас есть два параметра с разными именами: a.gt и a.ls Это означает, что наш SQL-запрос будет иметь операнд AND. Но для (2) у нас остались те же имена, и они должны быть преобразованы в SQL с операндом OR !

Это означает, что для (2) вместо использования #getParameterMap() мы должны напрямую анализировать URL-запрос и анализировать повторяющиеся имена параметров.