REST и сложные поисковые запросы

Я ищу надежный способ моделирования поисковых запросов в REST api.

В моем api вы можете указать критерии поиска в URI ресурса, используя параметры запроса.

Например:

/cars?search=color,blue;AND;doors,4 --> Returns a list of blue cars with 4 doors

/cars?search=color,blue;OR;doors,4 --> Returns a list of cars that are blue or have 4 doors

На стороне сервера строка поиска сопоставляется с требуемой базовой технологией. В зависимости от ресурса rest это может быть SQL-запрос, Hibernate Criteria api, другой вызов webservice,...

Два примера достаточно просты для поддержки, но мне также нужны более сложные функции поиска, такие как поиск подстроки, поиск до/после дат, NOT,...

Это общая проблема, я думаю. Есть ли библиотека (или шаблон), которую я могу использовать:

  • Поисковые запросы в Картах, заданные в виде строки, к общей модели критериев. Формат поиска не должен совпадать с приведенным выше.
  • Позволяет мне сопоставить модель Criteria с любой технологией. Мне нужно использовать.
  • Предлагает поддержку сопоставления Hibernate/JPA/SQL, но это бонус;)

С уважением,

Гленн

Ответ 1

Всякий раз, когда я сталкиваюсь с этими проблемами, я спрашиваю себя: "Как я могу представить это пользователю, если бы я создавал традиционную веб-страницу"? Простой ответ заключается в том, что я бы не представил такие варианты на одной странице. Интерфейс будет слишком сложным; однако я мог бы создать интерфейс, который позволил бы пользователям создавать все более сложные запросы по нескольким страницам и что решение, на которое, я думаю, вам следует пойти в этом случае.

Ограничение HATEOAS указывает, что мы должны включать в наши ответы элементы управления гиперссылками (ссылки и формы). Итак, скажем, у нас есть разбитые на страницы коллекции автомобилей /cars с опцией поиска, так что, когда вы получаете /cars, он возвращает что-то вроде (BTW, я использую пользовательский тип мультимедиа здесь, но формы и ссылки должны быть довольно очевидным. Сообщите мне, если это не так):

<cars href="/cars">
    <car href="/cars/alpha">...</car>
    <car href="/cars/beta">...</car>
    <car href="/cars/gamma">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?page=2"/>
    <search-color href="/cars" method="GET">
        <color type="string" cardinality="required"/>
        <color-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color-match>
        <color-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color-logic>
    </search>
    <search-doors href="/cars" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

Итак, просто скажем, что мы ищем белые автомобили, мы бы GET /cars?color=white, и мы могли бы получить что-то вроде:

<cars href="/cars?color=white">
    <car href="/cars/beta">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?color=white&page=2"/>
    <search-color href="/cars?color=white" method="GET">
        <color2 type="string" cardinality="required"/>
        <color2-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color2-match>
        <color2-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color2-logic>
    </search>
    <search-doors href="/cars?color=white" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

В результате мы уточним наш запрос. Так что просто скажем, что нам нужны белые автомобили, но не "не совсем белые" автомобили, мы могли бы GET '/cars? Color = white & color2 = off-white & color2-logic = not', которые могли бы вернуться

<cars href="/cars?color=white&color2=off-white&color2-logic=not">
    <car href="/cars/beta">...</car>
    <car href="/cars/delta">...</car>
    ...
    <next href="/cars?color=white&color2=off-white&color2-logic=not&page=2"/>
    <search-color href="/cars?color=white&color2=off-white&color2-logic=not" method="GET">
        <color3 type="string" cardinality="required"/>
        <color3-match type="enumeration" cardinality="optional" default="substring">
            <option name="exact"/>
            <option name="substring"/>
            <option name="regexp"/>
        </color3-match>
        <color3-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color3-logic>
    </search>
    <search-doors href="/cars?color=white&color2=off-white&color2-logic=not" method="GET">
        <doors type="integer" cardinality="required"/>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

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

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

<cars href="/cars">
    ...
    <search-doors href="/cars" method="GET">
        <doors type="enumeration" cardinality="required">
            <option name="2"/>
            <option name="3"/>
            <option name="4"/>
            <option name="5"/>
        </doors>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

Однако единственные белые автомобили, которые у нас есть, могут быть 2 и 4 двери, и в этом случае GETing /cars?color=white может дать нам

<cars href="/cars?color=white">
    ...
    <search-doors href="/cars?color=white" method="GET">
        <doors type="enumeration" cardinality="required">
            <option name="2"/>
            <option name="4"/>
        </doors>
        <door-logic type="enumeration" cardinality="required" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </door-logic>
    </search>
</cars>

Аналогично, поскольку мы уточняем цвета, мы можем найти их только в нескольких вариантах, и в этом случае мы можем перейти от предоставления строкового поиска к предоставлению поиска перечисления. например, GETing /cars?color=white может дать нам

<cars href="/cars?color=white">
    ...
    <search-color href="/cars?color=white" method="GET">
        <color2 type="enumeration" cardinality="required">
            <option name="white"/>
            <option name="off-white"/>
            <option name="blue with white racing stripes"/>
        </color2>
        <color2-logic type="enumeration" cardinality="optional" default="and">
            <option name="and"/>
            <option name="or"/>
            <option name="not"/>
        </color2-logic>
    </search>
    ...
</cars>

Вы можете сделать то же самое для других категорий поиска. Например, изначально вы не захотите перечислять все составляющие, поэтому вы бы предоставили какой-то текстовый поиск. После того, как коллекция была усовершенствована, и есть только несколько моделей, чтобы выбрать, тогда имеет смысл предоставить перечисление. Такая же логика применяется и к другим коллекциям. Например, вы не захотите перечислять все города мира, но как только вы усовершенствовали область до 10 или около того городов, то перечисление их может быть очень полезным.

Есть ли библиотека, которая сделает это за вас? Нет, о которых я знаю. Большинство из тех, что я видел, даже не поддерживают элементы управления гипермедиа (т.е. вам нужно добавить ссылки и формы). Есть ли образец, который вы можете использовать? Да, я считаю, что приведенное выше является допустимым шаблоном для решения этой проблемы.

Ответ 2

Хммм... это сложная область. Там нет рамок, которые адаптированы для вашей проблемы. Даже ODATA, о котором упоминается @p0wl, имеет определенные ограничения в отношении того, как поля отображаются на модели домена. Это становится странным после точки.

Самый простой способ обойти это - принять запрос как строку, которую вы затем проверяете на языке, специфичном для домена, с использованием AST или того, что вы хотите. Это позволяет вам быть гибким при принятии запроса, все еще проверяя его на правильность. То, что вы теряете, - это возможность более подробно описывать запрос через URL-адрес.

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

Ответ 3

@GlennV

Я не уверен, что есть шаблон/правило, напрямую связанные с этим, и если это общая проблема (я имею в виду логические операторы в строке запроса), но я задал бы следующий вопрос себе при разработке чего-то вроде этого:

  • Как клиент будет строить такие запросы?

Проблема заключается в том, что URI (в целом, включая часть строки запроса) должны полностью контролироваться поставщиком услуг, а клиенты не должны напрямую связываться с логикой построения URI, которые являются специфическими для вашего проблемного домена.

Существует несколько способов использования клиентами URI и способа их создания:

  • Клиент может следовать ссылкам, которые включены либо в заголовки HTTP через заголовок "Ссылка: <... > ", либо в виде тела (например, ссылки на атомы или HTML или что-то подобное).
  • Клиент может создавать URI по правилам, определенным типом медиа или другой спецификацией.

Несколько известных подходов к процессу построения управляемого URI:

  • HTML GET формы, в которых поля формы закодированы в соответствии с RFC3986 и добавлены к URI, указанному в значении атрибута формы (тип носителя );
  • Шаблоны URI (довольно широкие, хотя и заслуживающие чтения, спецификации предлагают действительно интересные функции) RFC6570
  • Открыть поисковые запросы (тип носителя)

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

Другое дело, как вы собираетесь сопоставлять URI с базовой реализацией, и есть две очень важные вещи с точки зрения REST:

  • Коннектор - который включает спецификации HTTP + URI и на самом деле только для коннекторов.
  • Компонент - это базовая реализация, и никто не заботится, кроме вас. Эта часть должна быть полностью скрыта от клиентов, и вы можете выбрать любой способ привязки здесь, никто не может точно сказать вам, что это полностью зависит от конкретных требований.

Ответ 4

Вы ищете такую ​​библиотеку, как ODATA (Ссылка)?

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

Ответ 5

Как уже упоминалось ранее, для этого не существует общего решения. Вероятно, лучший вариант (при наличии наилучшего ive) - Spring DATA REST (http://static.springsource.org/spring-data/rest/docs/1.1.0.M1/reference/htmlsingle/). Используя CrudRepository вы можете получить такие методы, как findOne, findAll и т.д. И, если вы его расширите, вы можете добавить свои собственные общие сопоставления. В примере, мы сделали это, чтобы расширить его, чтобы иметь общие параметры запроса, такие как: customer? country = notArgentina, чтобы получить клиентов, которые не принадлежат Аргентине. Или, например: customer? Country = like (Island), чтобы получить всех клиентов в стране как "Остров" (по-разному). Надеюсь, что это поможет.