it-roy-ru.com

Как спроектировать универсальные операторы фильтрации в строке запроса API?

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

  • Массив содержит: содержит ли properties.categoriesEngineering
  • Больше чем: является ли properties.created_at старше 2016-10-02
  • Не равно: не является ли properties.address.cityWashington
  • Равен: является ли properties.nameMeetup
  • и т.п.

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


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

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

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

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

Пример: API Stripe


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

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

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

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


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 верхнего уровня, аналогичный предыдущему, но он описывает операторы с помощью Word вместо определения их с помощью символов и имеет пробелы между ними. Это может быть немного более читабельным.

Но это происходит за счет наличия более длинного 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 верхнего уровня, но вместо создания полностью настраиваемого синтаксиса для него, имитирующего программирование, вместо этого он создает определение объектов для фильтров с использованием более стандартного синтаксиса строки запроса. Это дает преимущество чуть больше «стандарт».

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

Пример API Magento


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

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

13
Ian Storm Taylor

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

Во-первых, вы говорите о «универсальном API с контентом и схемой, которые могут быть определены пользователем».

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

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

Elasticsearch:

Они создали весь Query 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.

5
James Cube

# 4

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

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

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

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


# 2

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

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

1
Divyanshu Maithani

Вы также можете попробовать Spring Expression Language (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;
        }

    }
}
1
Sourabh

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

Предположим, некоторый параметр a должен быть равен 10 или 20. Наш URL-запрос в этом случае должен выглядеть как ?a.eq=10&a.eq=20 для (1) и ?a=eq:10&a=eq:20 для (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) у нас есть два параметра с разными names: a.gt и a.ls. Это означает, что наш SQL-запрос будет иметь операнд AND. Но для (2) у нас остались те же имена, и они должны быть преобразованы в SQL с операндом OR!

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

0
pto3

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

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

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

станет

/events?properties.name=='Harry'&&properties.address.city!='Washington'||properties.name=='Jack'&&properties.address.city!=('Paris','New Orleans ')

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

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

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

-k

0
schuttek