Как спроектировать RESTful поиск / фильтрацию? [закрыто]

457

В настоящее время я разрабатываю и внедряю RESTful API в PHP. Тем не менее, мне не удалось реализовать мой первоначальный дизайн.

GET /users # list of users
GET /user/1 # get user with id 1
POST /user # create new user
PUT /user/1 # modify user with id 1
DELETE /user/1 # delete user with id 1

Пока что довольно стандартно, правда?

Моя проблема с первым GET /users. Я рассматривал отправку параметров в теле запроса для фильтрации списка. Это потому, что я хочу иметь возможность указывать сложные фильтры без получения очень длинного URL, например:

GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4

Вместо этого я хотел иметь что-то вроде:

GET /users
# Request body:
{
    "parameter1": "value1",
    "parameter2": "value2",
    "parameter3": "value3",
    "parameter4": "value4"
}

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

Во всяком случае, file_get_contents('php://input')не вернул тело запроса для GETзапросов. Я тоже пробовал http_get_request_body(), но общего хостинга, который я использую, нет pecl_http. Не уверен, что это помогло бы в любом случае.

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

Так что теперь я не уверен, что делать. Как вы проектируете RESTful функцию поиска / фильтрации?

Я полагаю, я мог бы использовать POST, но это не выглядит очень RESTful.

Эрик Б
источник
7
Возможный дубликат дизайна RESTful URL для поиска
2012 г.
60
Быть осторожен!!! Метод GET должен быть IDEMPOTENT и должен быть «кэшируемым». Если вы отправляете информацию в теле Как система может кешировать ваш запрос? HTTP позволяет кэшировать GET-запрос, используя только URL, а не тело запроса. Например, эти два запроса: example.com {test: "some"} example.com {anotherTest: "some2"} рассматриваются системой кэширования одинаково: оба имеют одинаковый URL
jfcorugedo
15
Просто чтобы добавить, вы должны POST для / users (коллекция), а не / user (один пользователь).
Младен Б.
1
Еще один момент, на который следует обратить внимание: большинство серверов приложений имеют журналы доступа, в которых регистрируется URL-адрес, поэтому между ними может быть что-то среднее. Так что на GET может быть какая-то непреднамеренная утечка информации.
user3206144
2
Возможный дубликат дизайна RESTful URL для поиска
ivan_pozdeev

Ответы:

396

Лучший способ реализовать поиск RESTful - считать сам поиск ресурсом. Тогда вы можете использовать глагол POST, потому что вы создаете поиск. Вам не нужно буквально создавать что-то в базе данных, чтобы использовать POST.

Например:

Accept: application/json
Content-Type: application/json
POST http://example.com/people/searches
{
  "terms": {
    "ssn": "123456789"
  },
  "order": { ... },
  ...
}

Вы создаете поиск с точки зрения пользователя. Детали реализации этого не имеют значения. Некоторым API RESTful может даже не потребоваться постоянство. Это деталь реализации.

Джейсон Харрельсон
источник
209
Одним существенным ограничением использования запроса POST для конечной точки поиска является то, что он не может быть добавлен в закладки. Результаты поиска по закладкам (особенно сложные запросы) могут быть весьма полезными.
Каучанд
73
Использование POST для выполнения поиска может нарушить ограничение кеша REST. whatisrest.com/rest_constraints/cache_excerps
Филипе
56
Поиски, по своей природе, являются временными: данные развиваются между двумя поисками с одинаковыми параметрами, поэтому я думаю, что запрос GET не отображается точно в шаблон поиска. Вместо этого поисковый запрос должен быть POST (/ Resource / search), тогда вы можете сохранить этот поиск и перенаправить на результат поиска, например / Resource / search / iyn3zrt. Таким образом, запросы GET успешны и имеют смысл.
sleblanc
32
Я не думаю, что сообщение является подходящим методом для поиска, данные для обычных запросов GET тоже могут меняться со временем.
чудо
82
Это абсолютно худший из возможных ответов. Я не могу поверить, что у него так много голосов. Этот ответ объясняет, почему: programmers.stackexchange.com/questions/233164/…
Ричард
141

Если вы используете тело запроса в GET-запросе, вы нарушаете принцип REST, потому что ваш GET-запрос не сможет быть кэширован, потому что система кеширования использует только URL.

И что еще хуже, ваш URL не может быть добавлен в закладки, потому что URL не содержит всю информацию, необходимую для перенаправления пользователя на эту страницу

Используйте параметры URL или Query вместо параметров тела запроса.

например:

/myapp?var1=xxxx&var2=xxxx
/myapp;var1=xxxx/resource;var2=xxxx 

На самом деле HTTP RFC 7231 говорит, что:

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

Для получения дополнительной информации посмотрите здесь

jfcorugedo
источник
29
Учитесь на своей ошибке - я разработал API, используя предложенный ответ (POSTing JSON), но перехожу к параметрам URL. Возможность закладок может быть важнее, чем вы думаете. В моем случае возникла необходимость направлять трафик на определенные поисковые запросы (рекламная кампания). Кроме того, использование API истории имеет больше смысла с параметрами URL.
Джейк,
2
Это зависит от того, как его использовать. Если вы ссылаетесь на URL, который загружает страницу на основе этих параметров, это имеет смысл, но если главная страница выполняет вызов AJAX только для получения данных, основанных на параметрах фильтра, вы все равно не сможете добавить эту закладку в закладки, потому что она AJAX вызова и не имеет никакого отношения. Естественно, вы также можете добавить в закладки URL-адрес, который при переходе туда создает фильтр и отправляет POST-запрос к вызову ajax, и он будет работать просто отлично.
Даниэль Лоренц
@DanielLorenz Для лучшего взаимодействия с пользователем URL-адрес в этом случае должен быть изменен через History API. Я терпеть не могу, когда веб-сайт не позволяет использовать функциональность браузера для перехода на предыдущие страницы. И если это стандартная страница, созданная на стороне сервера, единственный способ сделать ее закладкой - использовать запрос GET. Кажется, что хорошие старые параметры запроса - лучшее решение.
Натан
@ Натан, я думаю, что неправильно понял этот ответ. Я говорил об использовании параметров строки запроса в get. Вы никогда не должны использовать параметры тела в вызове GET, потому что это было бы совершенно бесполезно. Я говорил больше о GET со строкой запроса, которую можно использовать / добавить в закладки, а затем при запуске страницы вы можете использовать эти параметры для создания фильтра в POST, используя эти параметры для получения данных. История все равно будет работать в этом сценарии.
Даниэль Лоренц
@DanielLorenz Ну ладно, это имеет смысл. Я думаю, что я неправильно понял, что вы говорили.
Натан
70

Кажется, что фильтрация / поиск ресурсов может быть реализован RESTful способом. Идея состоит в том, чтобы ввести новую конечную точку под названием /filters/или /api/filters/.

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

После создания такого фильтра есть две возможности получить результат поиска / фильтра.

  1. Новый ресурс с уникальным идентификатором будет возвращен вместе с 201 Createdкодом состояния. Затем с помощью этого идентификатора GETможно сделать запрос /api/users/:

    GET /api/users/?filterId=1234-abcd
    
  2. После создания нового фильтра POSTон не будет отвечать, 201 Createdно сразу 303 SeeOtherс Locationзаголовком, указывающим на /api/users/?filterId=1234-abcd. Это перенаправление будет автоматически обработано через базовую библиотеку.

В обоих сценариях необходимо сделать два запроса для получения отфильтрованных результатов - это может рассматриваться как недостаток, особенно для мобильных приложений. Для мобильных приложений я бы использовал один POSTвызов /api/users/filter/.

Как сохранить созданные фильтры?

Они могут храниться в БД и использоваться позже. Они также могут храниться в некотором временном хранилище, например, в redis, и иметь некоторое TTL, после которого они истекают и будут удалены.

Каковы преимущества этой идеи?

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

опал
источник
2
ну это должен быть принятый ответ. Вы не нарушаете принципы REST и можете делать длинные сложные запросы к ресурсам. Это красиво, чисто и совместимо с закладками. Единственным дополнительным недостатком является необходимость хранения пар ключ / значение для созданных фильтров и уже упомянутые два шага запроса.
Дантебарба
2
Единственная проблема, связанная с этим подходом, - это наличие в запросе фильтров даты и времени (или постоянно меняющихся значений). Тогда количество фильтров для хранения в БД (или кеше) неисчислимо.
Рви Пандей
17

Я думаю, что вы должны использовать параметры запроса, но только до тех пор, пока нет соответствующего HTTP-заголовка для выполнения того, что вы хотите сделать. В спецификации HTTP явно не сказано, что GET не может иметь тела. Однако в этой статье говорится:

По соглашению, когда используется метод GET, вся информация, необходимая для идентификации ресурса, кодируется в URI. В HTTP / 1.1 нет соглашения о безопасном взаимодействии (например, извлечении), когда клиент передает данные на сервер в теле объекта HTTP, а не в части запроса URI. Это означает, что для безопасных операций URI могут быть длинными.

Дафф
источник
6
ElasticSearch также делает GET с телом и работает хорошо!
Тарун Сапра
Да, но они контролируют реализацию сервера, но не могут быть связаны между собой.
user432024
7

Поскольку я использую бэкэнд laravel / php, я склонен делать что-то вроде этого:

/resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource

PHP автоматически превращает []params в массив, поэтому в этом примере я получу $filterпеременную, которая содержит массив / объект фильтров, а также страницу и любые связанные ресурсы, которые я хочу загрузить с нетерпением.

Если вы используете другой язык, это может быть хорошим соглашением, и вы можете создать парсер для преобразования []в массив.

-A-поезд
источник
Этот подход выглядит неплохо, но могут быть проблемы с использованием квадратных скобок в URL-адресах, посмотрите, что можно использовать в URL-адресе
Sky
2
@Sky Этого можно избежать с помощью URI, кодирующего [и ]. Использование закодированных представлений этих символов для группировки параметров запроса является хорошо известной практикой. Он даже используется в спецификации JSON: API .
Джелхан
6

Не беспокойтесь слишком сильно, если ваш начальный API полностью RESTful или нет (особенно когда вы находитесь на стадии альфа). Сначала включите работу сантехники. Вы всегда можете выполнить какое-то преобразование / переписывание URL-адреса, чтобы наметить все, уточняя итеративно, пока не получите что-то достаточно стабильное для широкого тестирования («бета»).

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

.ie. Выполните поиск «пользовательского» типа с параметром [i] = значение [i] для i = 1..4 в хранилище № 1 (со значением 1, значением 2, значением 3, ... в качестве сокращения для параметров запроса URI):

1) GET /store1/search/user/value1,value2,value3,value4

или

2) GET /store1/search/user,value1,value2,value3,value4

или как следует (хотя я бы не советовал, об этом позже)

3) GET /search/store1,user,value1,value2,value3,value4

При выборе варианта 1 вы отображаете все URI с префиксом /store1/search/userв обработчик поиска (или любое другое обозначение PHP) по умолчанию для поиска ресурсов в хранилище store1 (эквивалентно /search?location=store1&type=user.

По соглашению, задокументированному и применяемому API, значения параметров с 1 по 4 разделяются запятыми и представляются в указанном порядке.

Вариант 2 добавляет тип поиска (в данном случае user) в качестве позиционного параметра # 1. Любой из вариантов - просто косметический выбор.

Вариант 3 также возможен, но я не думаю, что мне это понравится. Я думаю, что возможность поиска в определенных ресурсах должна быть представлена ​​в самом URI, предшествующем самому поиску (как если бы в URI четко указывалось, что поиск специфичен для ресурса).

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

Как только вы сделаете что-то подобное, вы можете использовать GET, и это будет ресурс только для чтения (так как вы не можете POST или PUT к нему - он обновляется, когда он GET'ed). Это также будет ресурс, который появляется только при его вызове.

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

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

luis.espinal
источник
7
Эй, если вы (кто-то, кто бы ни / что бы) не захотели преуменьшить мой ответ, было бы больно вам эго хотя бы оставить комментарий, указывающий, с чем именно вы не согласны? Я знаю, что это interweebz, но ...;)
luis.espinal
107
Я не понизил голос, но тот факт, что вопрос начинается с: «Я сейчас занимаюсь проектированием и реализацией RESTful API», а ваш ответ начинается с «Не беспокойтесь слишком сильно, если ваш начальный API полностью RESTful или нет», чувствует неправильно со мной. Если вы разрабатываете API, вы разрабатываете API. Вопрос в том, как лучше спроектировать API, а не в том, должен ли быть спроектирован API.
Гардарх
14
API - это система, сначала работа с API, а не с бэкендом, первая реализация может / должна быть просто фальшивкой. HTTP имеет механизм для передачи параметров, вы предлагаете его заново изобрести, но хуже (упорядоченные параметры вместо пар имя-значение). Отсюда и голосование против.
Стивен Ирод
14
@gardarh - да, это неправильно, но иногда это прагматично. Основная задача - разработать API, который будет работать в бизнес-контексте. Если подход RESTFULL подходит для бизнеса под рукой, то пойти на это. Если это не так, то не делайте этого. То есть разработайте API, соответствующий вашим конкретным бизнес-требованиям. Попытка сделать его RESTfull как его основное требование мало чем отличается от вопроса: «Как использовать шаблон адаптера в задаче X / Y». Не обувайте парадигмы рожка, если они не решают реальные, ценные проблемы.
luis.espinal
1
Я рассматриваю ресурс как некоторую совокупность состояний, а параметры - как средство для параметрического управления представлением этого состояния. Подумайте об этом так, если бы вы могли использовать ручки и переключатели для настройки способа отображения ресурса (показать / скрыть определенные биты, упорядочить его по-другому и т. Д.), Эти элементы управления являются параметрами. Если это на самом деле другой ресурс (например, '/ album' vs '/ artist'), тогда он должен быть представлен в пути. Это то, что для меня интуитивно понятно.
Эрик Эллиот
2

К вашему сведению: я знаю, что уже немного поздно, но для тех, кто заинтересован. Зависит от того, насколько RESTful вы хотите быть, вам придется реализовать свои собственные стратегии фильтрации, так как спецификация HTTP не очень ясна по этому поводу. Я хотел бы предложить URL-кодирование всех параметров фильтра, например

GET api/users?filter=param1%3Dvalue1%26param2%3Dvalue2

Я знаю, что это некрасиво, но я думаю, что это самый ОТЛИЧНЫЙ способ сделать это, и его должно быть легко разобрать на стороне сервера :)

черенки
источник