Разработка REST API по URI против строки запроса

73

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

Grandparent (collection) -> Parent (collection) -> and Child (collection)

Выше показано отношение между этими ресурсами примерно так: каждый дедушка может сопоставить с одним или несколькими родителями. Каждый родитель может сопоставить одному или нескольким детям. Мне нужна возможность поддержки поиска по дочернему ресурсу, но с критериями фильтра:

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

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

Я думал о чем-то вроде этого:

GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

а также

GET /myservice/api/v1/parents/{parentID}/children?search={text}

для вышеуказанных требований соответственно.

Но я также мог бы сделать что-то вроде этого:

GET /myservice/api/v1/children?search={text}&grandparentID={id}&parentID=${id}

В этом проекте я мог позволить своему клиенту передавать мне одно или другое в строке запроса: либо grandparentID, либо parentID, но не оба.

Мои вопросы:

1) Какой дизайн API более RESTful и почему? Семантически они имеют в виду и ведут себя одинаково. Последний ресурс в URI - это «дети», что фактически означает, что клиент работает с дочерним ресурсом.

2) Каковы плюсы и минусы каждого с точки зрения понятности с точки зрения клиента и ремонтопригодности с точки зрения дизайнера.

3) Для чего действительно используются строки запроса, помимо «фильтрации» на вашем ресурсе? Если вы выберете первый подход, параметр фильтра будет встроен в сам URI как параметр пути, а не как параметр строки запроса.

Спасибо!

HiChews123
источник
3
Название вашего вопроса должно быть крайне запутанным для всех, кто его просматривает. Допустимые сегменты URI определены как <схема>: // <пользователь>: <пароль> @ <хост>: <порт> / <путь>; <параметры>? <Запрос> / # <фрагмент> (хотя < пароль> устарел) «Строка запроса» является допустимым компонентом URI, поэтому ваши «vs» в заголовке - это ненормальный разговор.
К. Алан Бейтс
Вы имеете в виду I want to only search against children who are INdirect descendants of that grandparent.? Согласно вашей структуре, дедушка не имеет прямых детей.
ноль
В чем разница между ребенком и родителем? Является ли родитель родителем, если у него нет детей?
Запах
re: potential design flawи если у вас есть информация о человеке, но нет информации о его родителях, они квалифицируются как child? (например, Адам и Ева) :)
Джесси Чисхолм

Ответы:

64

Первый

Согласно RFC 3986 §3.4 (Унифицированные идентификаторы ресурсов § (Синтаксические компоненты) | Запрос

3.4 Запрос

Компонент запроса содержит неиерархические данные, которые вместе с данными в компоненте пути (раздел 3.3) служат для идентификации ресурса в рамках схемы URI и полномочий по присвоению имен (если таковые имеются).

Компоненты запросов предназначены для поиска неиерархических данных; Есть несколько вещей, более иерархических по своей природе, чем родословная! Ergo - независимо от того, думаете ли вы, что это «REST-y» или нет - для того, чтобы соответствовать форматам, протоколам и структурам и для разработки систем в Интернете, вы не должны использовать строку запроса для идентификации этой информации.

REST не имеет ничего общего с этим определением.

Прежде чем ответить на ваши конкретные вопросы, ваш параметр запроса "search" имеет неправильное имя. Лучше было бы рассматривать ваш сегмент запроса как словарь пар ключ-значение.

Строка запроса может быть более правильно определена как

?first_name={firstName}&last_name={lastName}&birth_date={birthDate} и т.п.

Чтобы ответить на ваши конкретные вопросы

1) Какой дизайн API более RESTful и почему? Семантически они имеют в виду и ведут себя одинаково. Последний ресурс в URI - это «дети», что фактически означает, что клиент работает с дочерним ресурсом.

Я не думаю, что это так ясно, как вы, кажется, верите.

Ни один из этих интерфейсов ресурсов не является RESTful. Основным условием для RESTful архитектурного стиля является то , что состояние приложения переходы должны быть переданы с сервера в гипермедиа. Люди работали над структурой URI, чтобы сделать их как-то «RESTful URI», но формальная литература о REST на самом деле очень мало говорит об этом. Мое личное мнение таково, что большая часть мета-дезинформации о REST была опубликована с намерением избавиться от старых вредных привычек. (Построение действительно «RESTful» системы на самом деле довольно трудоемко. Индустрия взяла верх над «REST» и наполнила некоторые ортогональные проблемы бессмысленными оговорками и ограничениями.)

В литературе по REST говорится, что если вы собираетесь использовать HTTP в качестве протокола приложения, вы должны придерживаться формальных требований спецификаций протокола, и вы не можете «создать http по ходу и при этом заявить, что используете http» ; если вы собираетесь использовать URI для идентификации своих ресурсов, вы должны придерживаться формальных требований спецификаций, касающихся URI / URL.

Ваш вопрос адресован непосредственно RFC3986 §3.4, который я связал выше. Суть в том, что даже если соответствующего URI недостаточно для того, чтобы считать API «RESTful», если вы хотите, чтобы ваша система действительно была «RESTful», и вы используете HTTP и URI, то вы не можете идентифицировать иерархические данные через Строка запроса, потому что:

3.4 Запрос

Компонент запроса содержит неиерархические данные

... это так просто.

2) Каковы плюсы и минусы каждого с точки зрения понятности с точки зрения клиента и ремонтопригодности с точки зрения дизайнера.

«Плюсы» первых двух в том, что они на правильном пути . «Минусы» третьего - это то, что он кажется неправильным.

Что касается вашей понятности и удобства сопровождения, то они определенно субъективны и зависят от уровня понимания разработчика клиента и выбора дизайна дизайнера. Спецификация URI является окончательным ответом относительно того, как URI должны быть отформатированы. Предполагается, что иерархические данные представлены на пути и с параметрами пути. Неиерархические данные должны быть представлены в запросе. Фрагмент является более сложным, потому что его семантика зависит конкретно от типа носителя запрашиваемого представления. Итак, чтобы рассмотреть компонент «понятности» вашего вопроса, я попытаюсь перевести именно то, что на самом деле говорят ваши первые два URI. Затем я попытаюсь представить то, что, как вы говорите, вы пытаетесь выполнить с помощью действительных URI.

Перевод ваших дословных URI в их семантическое значение. /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} Это говорит о том, что для родителей бабушек и дедушек найдите, что их ребенок имеет то, search={text} что вы сказали с помощью вашего URI, только связно, если вы ищете родных братьев и дедушек. С вашими «бабушкой и дедушкой, родителями, детьми» вы обнаружили, что «дедушка и бабушка» прошли поколение к своим родителям, а затем вернулись к поколению «бабушки и дедушки», посмотрев на детей родителей.

/myservice/api/v1/parents/{parentID}/children?search={text} Это говорит о том, что для родителя, идентифицированного {parentID}, найдите, что у его потомка есть ?search={text}Это ближе к тому, что вы хотите, и представляет отношение родитель-потомок, которое, вероятно, может использоваться для моделирования всего API. Чтобы смоделировать это таким образом, на клиента ложится бремя, чтобы признать, что если у него есть «grandparentId», то существует слой косвенности между идентификатором, который он имеет, и той частью семейного графа, которую он хочет видеть. Чтобы найти «child» с помощью «grandparentId», вы можете позвонить в свою /parents/{parentID}/childrenслужбу, а затем найти каждого ребенка, которого вернули, и найти среди своих детей идентификатор вашей личности.

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

1) Первый, о котором я уже говорил. Представьте граф «Люди» в виде составной структуры. У каждого человека есть ссылка на поколение над ним через его путь Родителей и на поколение под ним через его путь Детей.

/Persons/Joe/Parents/Mother/Parents был бы способ схватить бабушку и дедушку Джо по материнской линии.

/Persons/Joe/Parents/Parents был бы способ схватить всех бабушек и дедушек Джо.

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} схватил бы бабушку и дедушку Джо с идентификатором у вас в руках.

и все это имело бы смысл (обратите внимание, что здесь может быть снижение производительности в зависимости от задачи путем принудительной установки dfs на сервере из-за отсутствия идентификации филиала в шаблоне «Родители / Родители / Родители».) Вам также полезно иметь способность поддерживать любое произвольное количество поколений. Если по какой-то причине вы хотите посмотреть на 8 поколений, вы можете представить это как

/Persons/Joe/Parents/Parents/Parents/Parents/Parents/Parents/Parents/Parents?id={Joe.NotableAncestor}

но это приводит ко второму доминирующему варианту представления этих данных: через параметр пути.


2) Использование параметров пути для «запроса иерархии». Вы можете разработать следующую структуру, чтобы облегчить нагрузку на потребителей, и при этом иметь API, который имеет смысл.

Чтобы оглянуться назад на 147 поколений, представление этого идентификатора ресурса с параметрами пути позволяет сделать

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor}

Чтобы отыскать Джо у его прародителя, вы можете посмотреть на график известного количества поколений для идентификатора Джо. /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

При использовании этих подходов следует отметить, что без дополнительной информации в идентификаторе и запросе следует ожидать, что первый URI извлекает человека из поколения 147 от Джо с идентификатором Joe.NotableAncestor. Вы должны ожидать, что второй заберет Джо. Предположим, что вы действительно хотите, чтобы вызывающий клиент мог получить весь набор узлов и их взаимосвязи между корневым лицом и конечным контекстом вашего URI. Вы можете сделать это с тем же URI (с некоторым дополнительным оформлением) и настройкой Accept of text/vnd.graphvizon по вашему запросу, который является зарегистрированным IANA типом носителя для .dotпредставления графа. С этим измените URI на

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor.Id}#directed

с заголовком HTTP-запроса, Accept: text/vnd.graphviz и вы можете достаточно четко проинформировать клиентов о том, что им нужен ориентированный граф иерархии поколений между Джо и 147 поколениями до того, когда это 147-е поколение предков содержит человека, идентифицированного как «Известный предок» Джо.

Я не уверен, что text / vnd.graphviz имеет какую-либо заранее определенную семантику для своего фрагмента, я не мог найти ни одной в поиске инструкции. Если этот тип мультимедиа действительно имеет предварительно определенную информацию о фрагменте, то его семантика должна соблюдаться для создания соответствующего URI. Но, если эта семантика не предопределена, спецификация URI заявляет, что семантика идентификатора фрагмента не ограничена и вместо этого определяется сервером, что делает это использование допустимым.


3) Для чего действительно используются строки запроса, помимо «фильтрации» на вашем ресурсе? Если вы выберете первый подход, параметр фильтра будет встроен в сам URI как параметр пути, а не как параметр строки запроса.

Я полагаю, что уже полностью избил это до смерти, но строки запроса не для "фильтрации" ресурсов. Они предназначены для идентификации вашего ресурса по неиерархическим данным. Если вы углубились в иерархию, указав свой путь, /person/{id}/children/и хотите указать конкретный дочерний элемент или определенный набор дочерних элементов, вы должны использовать какой-либо атрибут, который применяется к идентифицируемому набору, и включить его в запрос.

К. Алан Бейтс
источник
1
RFC касается только иерархии, поскольку он определяет синтаксис и алгоритм для разрешения относительных ссылок URI. Не могли бы вы уточнить или привести некоторые источники, объясняющие, почему примеры в оригинальном посте не соответствуют?
user2313838
2
Разве семейное древо на самом деле не граф, а не дерево и вовсе не иерархическое. учитывая множественные родители, развод и повторный брак и т. д.
Myster
1
@RobertoAloi Мне кажется нелогичным передавать свой собственный интерфейс «No Items Found» через пустой набор, когда в HTTP уже есть определение для этого. Основной принцип заключается в том, что вы просите сервер вернуть «вещь (и)», и если нет «вещи (ий)» для возврата, сервер сообщает об этом «404 - Не найдено». Что противоречит этому?
К. Алан Бейтс
1
Я всегда полагал, что 404 указывает на то, что корневой URL ресурса не был найден, то есть коллекция в целом. Так что, если вы запросили / books? Author = Jim и у jim не было книг, вы получите пустой набор []. Но если бы вы запросили / article? Author = Jim, но ресурс статей даже не существовал как коллекция 404, это помогло бы указать, что вообще нет смысла искать какие-либо статьи.
adjenks
1
@adjenks Вы не узнали это из формальных спецификаций. Структурно, составляющие URL-адреса могут рассматриваться как содержащие «корень», если это помогает вам рассуждать о целях составных частей, но в конечном итоге строка запроса не является фильтром отображения для ресурса, идентифицированного по пути. Строка запроса является первоклассным гражданином URL. Если на сервере нет ресурсов, соответствующих вашему URL-адресу (включая строку запроса), 404 - это средство, определенное в http для передачи этого. Модель взаимодействия, которую вы представляете, вводит различие без разницы.
К. Алан Бейтс
14

Вот где вы ошибаетесь:

Если мои клиенты передадут мне ссылку на идентификатор

В системах REST клиент никогда не должен беспокоиться об идентификаторах. Единственные идентификаторы ресурса, о которых клиент должен знать, должны быть URI. Это принцип "единого интерфейса".

Подумайте, как клиенты будут взаимодействовать с вашей системой. Скажем, пользователь начинает просматривать список бабушек и дедушек, он выбрал одного из детей бабушки и дедушки, что приводит его к /grandparent/123. Если клиент должен иметь возможность поиска /grandparent/123по дочерним элементам , то, согласно «HATEOAS», все, что возвращалось при выполнении запроса, /grandparent/123должно возвращать URL в интерфейс поиска. Этот URL должен уже содержать все данные, необходимые для фильтрации по текущему деду, встроенному в него.

Если ссылка выглядит /grandparent/123?search={term}или /parent?grandparent=123&search={term}или /parent?grandparentTerm=someterm&someothergplocator=blah&search={term}несущественны в соответствии с REST. Обратите внимание, что все эти URL имеют одинаковое количество параметров, {term}хотя они используют разные критерии. Вы можете переключаться между любыми из этих URL-адресов или смешивать их в зависимости от конкретных бабушек и дедушек, и клиент не сломается, потому что логические отношения между ресурсами одинаковы, даже если базовая реализация может значительно отличаться.

Если бы вместо этого вы создали службу так, что она требует, /grandparent/{grandparentID}?search={term}когда вы идете одним путем, а /children?parent={parentID}&search={term}}, когда вы идете другим путем, это слишком большая связь, потому что клиент должен был бы знать, чтобы интерполировать разные вещи в разных отношениях, которые концептуально похожи.

Независимо от того, идете ли вы по вкусу, /grandparent/123?search={term}или /parent?grandparent=123&search={term}это дело вкуса и того, какая реализация вам проще. Важно не требовать изменения клиента, если вы измените свою стратегию URL или если вы используете разные стратегии в отношениях между родителями и детьми.

Ли Райан
источник
10

Я не уверен, почему люди думают, что размещение значений идентификаторов в URL означает, что это REST API, а REST - это обработка глаголов, передача ресурсов.

Поэтому, если вы хотите PUT нового пользователя, вам нужно будет отправить достаточное количество данных, и HTTP-запрос POST идеален, поэтому, хотя вы можете отправить ключ (например, идентификатор пользователя), вы отправите данные пользователя (например, имя, адрес) в качестве данных POST.

В настоящее время общепринятым является размещение идентификатора ресурса в URI, но это более условно, чем любая форма канонического «его не REST, если его нет в URI». Помните, что первоначальный тезис о REST на самом деле вообще не упоминает http, это идиома для обработки данных на клиент-сервере, а не то, что является расширением http (хотя, очевидно, http является нашей основной формой реализации REST) ,

Например, Филдинг использует запрос академической бумаги в качестве примера. Вы хотите получить ресурс «Документ доктора Джона о выпивке пива», но вам также может понадобиться начальная версия или последняя версия, поэтому идентификатор ресурса может быть не тем, на который легко ссылаться как одним идентификатором, который может быть помещается в URI. REST позволяет это сделать, и заявленное намерение заключается в следующем:

Вместо этого REST полагается на то, что автор выбирает идентификатор ресурса, который лучше всего соответствует характеру идентифицируемой концепции.

Так что ничто не мешает вам использовать статический URI для извлечения ваших родителей, передавая поисковый запрос в строке запроса, чтобы определить, какого именно пользователя вы ищете. В этом случае «ресурс», который вы идентифицируете, - это набор бабушек и дедушек (и поэтому URI содержит «бабушку и дедушку» как часть URI. REST включает в себя понятие «контрольные данные», предназначенное для определения того, какое представление вашего ресурс должен быть извлечен - пример, приведенный в этих документах, - это управление кэшем, но также и управление версиями, поэтому мой запрос на превосходную работу д-ра Джона может быть уточнен путем передачи версии в качестве управляющих данных, а не части URI.

Я думаю, что примером интерфейса REST, который обычно не упоминается, является SMTP . При создании почтового сообщения вы отправляете глаголы (FROM, TO и т. Д.) С данными ресурса для каждой части почтового сообщения. Это RESTful, даже если он не использует глаголы http, он использует свой собственный набор.

Так что ... хотя вам нужно иметь некоторую идентификацию ресурса в вашем URI, это не обязательно должна быть ваша идентификационная ссылка. Это можно с радостью отправить в виде управляющих данных, в строке запроса или даже в данных POST. Что вы действительно определяете в своем REST API, так это то, что вы ищете ребенка, который уже есть в вашем URI.

Так что, на мой взгляд, читая определение REST, вы запрашиваете дочерний ресурс, передавая управляющие данные (в форме строки запроса), чтобы определить, какой из них вы хотите вернуть. В результате вы не можете сделать запрос URI для деда или родителя. Вы хотите, чтобы дети возвращались, поэтому термин родитель / дедушка или идентификатор определенно не должен быть в таком URI. Дети должны быть.

gbjbaanb
источник
На самом деле, URI как концепция занимает центральное место в REST. Что не так важно, хотя это синтаксис URI. Правильный интерфейс REST должен идентифицировать ресурсы с общим синтаксисом. Исходя из принципа REST, не обязательно плохо идентифицировать сложный ресурс с помощью объекта JSON вместо URI RFC 3986, хотя, если вы это сделаете, вам следует использовать JSON RI для всего API.
Ли Райан
4

Многие люди уже говорили о том, что означает REST и т. Д. И т. Д. Но, похоже, никто не решает реальную проблему: ваш дизайн.

Почему дедушка отличается от отца? у них обоих есть дети, которые могут иметь детей, которые могут ...

В конце концов, они все «люди». Вы, вероятно, также имеете это в своем коде. Так что используйте это:

GET /<api-endpoint>/humans/<ID>

Вернет некоторую полезную информацию о человеке. (Имя и прочее)

GET /<api-endpoint>/humans/<ID>/children

очевидно, вернет массив детей. Если дочерних элементов не существует, возможно, стоит использовать пустой массив.

Чтобы сделать это проще, вы можете, например, добавить флаг hasChildren. или «hasGrandChildren».

Думай умнее, не сложнее

Pinoniq
источник
1

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

GET /myservice/api/v1/grandparents/{grandparentID}
GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

Поиск параметров запроса - хороший способ выполнить поиск в контексте этого ресурса.

Когда семья становится очень большой, вы можете использовать старт / лимит в качестве параметров запроса, например:

GET /myservice/api/v1/grandparents/{grandparentID}/children?start=1&limit=50

Как разработчику, хорошо иметь разные ресурсы с уникальным URL / URI. Я думаю, что вы должны использовать параметр запроса только тогда, когда они также могут быть опущены.

Может быть, это хорошее прочтение http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling и в остальном оригинальная кандидатская диссертация Роя Т Филдинга https://www.ics.uci.edu /~fielding/pubs/dis Диссертация/fielding_dis диссертации.pdf, которая объясняет концепции очень хорошо и полно.

l.renkema
источник
1

Я пойду с

/myservice/api/v1/people/{personID}/descendants/2?search={text}

В этом случае каждый префикс является допустимым ресурсом:

  • /myservice/api/v1/people: все люди
  • /myservice/api/v1/people/{personID}: один человек с полной информацией (включая предков, братьев и сестер, потомков)
  • /myservice/api/v1/people/{personID}/descendants: потомки одного человека
  • /myservice/api/v1/people/{personID}/descendants/2: внуки одного человека

Может быть, не самый лучший ответ, но, по крайней мере, имеет смысл для меня.

laike9m
источник
-1

многие до меня уже указывали, что формат URL несущественен в контексте службы RESTful ... мои два цента были бы ... важным аспектом REST, о чем говорилось в первоначальной статье, было понятие "Ресурсы" .. . когда вы разрабатываете свой URL, вам, возможно, нужно помнить один аспект: «Ресурс» - это не строка в одной таблице (хотя это может быть). Более того, это представление. Это также согласуется. .и могут быть использованы для внесения изменений в состояние этого представления (и, фактически, в нижележащий носитель) ... и данный ресурс может иметь смысл только в вашем бизнес-контексте ... например;

В вашем примере / myservice / api / v1 / grandparents / {grandparentID} / родителей / детей? Search = {текст}

Может быть более значимым ресурсом, если вы сократите это / myservice / api / v1 / siblingsOfGrandParents? Search = ..

в этом случае вы можете объявить 'siblingsOfGrandParents' как ресурс .. это упрощает URL

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

Сенту Сивасамбу
источник
когда вы тратите свое драгоценное время на уклонение от ответа, было бы полезно, если бы вы могли высказаться и изложить свои доводы.
Сенту Сивасамбу
Я не совсем уверен, почему понизили на основе чтения вашего ответа. Я согласен. Несколько непосредственных вещей, которые бросаются в глаза, это то, что вы привели слегка касательный пример, упомянув «братьев и сестер», когда ОП спрашивал об иерархических отношениях. Возможно, ваш стиль письма может быть улучшен. Вы используете много «...», мы обычно не используем «...» в официальной, профессиональной или учебной письменности. Также есть некоторая избыточность (например, вы упоминаете, например, затем говорите в своем примере). Возможно, ваш ответ мог бы быть более кратким и более непосредственным в отношении вопроса ОП.
KennyCason