Каковы лучшие практики для вложенных ресурсов REST?

301

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

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

Отдел не может существовать без ассоциированной компании.

Сотрудник не может существовать без ассоциированного отдела.

Теперь я нашел бы естественное представление шаблонов ресурсов.

  • /companies Коллекция компаний - Принимает положил для новой компании. Получи за всю коллекцию.
  • /companies/{companyId}Индивидуальная компания. Принимает GET, PUT и DELETE
  • /companies/{companyId}/departmentsПринимает POST для нового элемента. (Создает отдел внутри компании.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

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

Однако моя проблема возникает, если я хочу перечислить ( GET) всех сотрудников во всех компаниях.

Шаблон ресурсов для этого наиболее точно соответствует /employees(Коллекция всех сотрудников)

Означает ли это, что я должен иметь /employees/{empId}также потому, что если так, то есть два URI, чтобы получить тот же ресурс?

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

На базовом уровне /employees/?company={companyId}&department={deptId}возвращается точно такое же представление о сотрудниках, как и наиболее глубоко вложенный шаблон.

Какова оптимальная практика для шаблонов URL, когда ресурсы принадлежат другим ресурсам, но должны обрабатываться отдельно?

Wes
источник
1
Это почти точно противоположная проблема, описанная в stackoverflow.com/questions/7104578/…, хотя ответы могут быть связаны. Оба вопроса касаются владения, но этот пример подразумевает, что объект верхнего уровня не является владельцем.
Уэс
1
Именно то, что мне было интересно. Для данного варианта использования ваше решение кажется хорошим, но что, если отношение является агрегацией, а не композицией? Все еще пытаемся выяснить, какая здесь лучшая практика ... Кроме того, это решение подразумевает только создание отношений, например, работает уже существующий человек, или оно создает объект человека?
Якоб О.
Это создает человека в моем вымышленном примере. Причина, по которой я использовал эти доменные термины, - достаточно понятный пример, хотя и имитирующий мою реальную проблему. Вы смотрели через связанный вопрос, который может больше помочь вам в ухудшении отношений.
Wes
Я разделил свой вопрос на ответ и вопрос.
Уэст

Ответы:

152

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

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

Только потому, что сотрудники доступны в отделе:

company/{companyid}/department/{departmentid}/employees

Это не значит, что они не могут быть доступны и в компании:

company/{companyid}/employees

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

Но я надеюсь, что все обработчики URL-адресов используют один и тот же код поддержки для удовлетворения запросов, чтобы вы не дублировали код.

jeremyh
источник
11
Это указывает на дух RESTful, нет правил, которые говорят, что вы должны или не должны делать, если только вы сначала рассматриваете значимый ресурс . Но, кроме того, мне интересно, как лучше не дублировать код в таких сценариях.
abookyun
13
@abookyun, если вам нужны оба маршрута, то повторяющийся код контроллера между ними может быть абстрагирован к сервисным объектам.
bgcode
Это не имеет ничего общего с REST. REST не заботится о том, как вы структурируете часть пути ваших URL-адресов ... все, о чем он заботится, это действительные, надеюсь,
надежные
Основываясь на этом ответе, я думаю, что любой API, где все динамические сегменты являются уникальными идентификаторами, не должен обрабатывать несколько динамических сегментов ( /company/3/department/2/employees/1). Если API предоставляет способы получить каждый ресурс, то выполнение каждого из этих запросов может быть выполнено либо в библиотеке на стороне клиента, либо в виде одноразовой конечной точки, которая повторно использует код.
максимум
1
Хотя запрета нет, я считаю более изящным иметь только один путь к ресурсу - сохранять все ментальные модели проще. Я также предпочитаю, чтобы URI не меняли свой тип ресурса при наличии вложенности. например, /company/*должен только возвращать ресурс компании и не изменять тип ресурса вообще. Ничто из этого не указано в REST - обычно это плохо указано - просто личное предпочтение.
Кашиф
174

Я испробовал обе стратегии проектирования - вложенные и не вложенные конечные точки. Я нашел это:

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

  2. вложенные конечные точки обычно требуют избыточных конечных точек. Другими словами, вам чаще всего понадобится дополнительная конечная точка / employee, чтобы вы могли получить список сотрудников по отделам. Если у вас есть / сотрудники, что именно / компании / отделы / сотрудники покупают вам?

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

  4. иногда ресурс может иметь несколько типов родителей. В результате все конечные точки возвращают один и тот же ресурс.

  5. избыточные конечные точки затрудняют написание документов, а также затрудняют изучение API.

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

ПАТС
источник
24
Было очень приятно встретить этот ответ. Я использую вложенные конечные точки уже несколько месяцев после того, как меня учили, что это «правильный путь». Я пришел ко всем тем же выводам, которые вы перечислили выше. Так гораздо проще с не вложенным дизайном.
user3344977
6
Вы, кажется, перечислили некоторые недостатки как недостатки. «Просто добавьте больше параметров в одну конечную точку» делает API труднее документировать и изучать, а не наоборот. ;-)
Дренми
4
Не фанат этого ответа. Нет необходимости вводить избыточные конечные точки только потому, что вы добавили вложенный ресурс. Также не проблема, чтобы один и тот же ресурс возвращался несколькими родителями, если эти родители действительно владеют вложенным ресурсом. Это не проблема, чтобы получить родительский ресурс, чтобы узнать, как взаимодействовать с вложенными ресурсами. Хороший обнаруживаемый REST API должен сделать это.
Скотт
3
@Scottm - Один из недостатков вложенных ресурсов, с которыми я столкнулся, заключается в том, что это может привести к возвращению неверных данных, если идентификаторы родительских ресурсов неверны / не совпадают. Если предположить, что проблем с авторизацией нет, то до реализации API оставляется проверка того, что вложенный ресурс действительно является дочерним по отношению к родительскому ресурсу, который передается. Если эта проверка не закодирована, ответ API может быть неправильным, что приведет к повреждению. Что ты думаешь?
Энди Дафресн
1
Вам не нужны промежуточные родительские идентификаторы, если все конечные ресурсы имеют уникальные идентификаторы. Например, чтобы получить сотрудника по идентификатору, у вас есть GET / companies / департаменты / сотрудники / {empId}, или чтобы получить всех сотрудников в компании 123, у вас есть GET / companies / 123 / отделы / сотрудники / Сохранение иерархического пути делает более очевидным, как Вы можете получить промежуточные ресурсы для фильтрации / создания / изменения и помогает с возможностью обнаружения, на мой взгляд.
PaulG
77

Я переместил то, что сделал, с вопроса на ответ, где больше людей, вероятно, увидят это.

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

Так что в этом примере (просто перечисление конечных точек, которые изменяют ресурс)

  • POST /companies/ создает новую компанию, возвращает ссылку на созданную компанию.
  • POST /companies/{companyId}/departments когда отдел помещен, создается новый отдел возвращает ссылку на /departments/{departmentId}
  • PUT /departments/{departmentId} изменяет отдел
  • POST /departments/{deparmentId}/employees создает нового сотрудника возвращает ссылку на /employees/{employeeId}

Таким образом, есть ресурсы корневого уровня для каждой из коллекций. Однако создание находится в владеющем объекте.

Wes
источник
4
Я придумал такой же тип дизайна. Я думаю, что интуитивно понятно создавать такие вещи «там, где они принадлежат», но при этом иметь возможность перечислять их глобально. Тем более, когда есть отношения, где ресурс ДОЛЖЕН иметь родителя. Тогда создание этого ресурса в глобальном масштабе не делает это очевидным, но делать это на подобъекте, подобном этому, имеет смысл.
Иоаким
Я думаю, что вы использовали POSTсмысл PUT, и в противном случае.
Херардо Лима
На самом деле нет Обратите внимание, что я не использую предварительно назначенные идентификаторы для создания, так как сервер в этом случае отвечает за возврат идентификатора (в ссылке). Таким образом, написание POST является правильным (не может получить на той же реализации). Однако пут изменяет весь ресурс, но он все еще доступен в том же месте, поэтому я помещаю его. PUT vs POST - это другой вопрос, и он тоже спорный. Например stackoverflow.com/questions/630453/put-vs-post-in-rest
Уэс
@Wes Даже я предпочитаю модифицировать методы глагола, чтобы быть под родителем. Но видите ли вы, что передаваемый параметр запроса для глобального ресурса приемлем? Пример: POST / отделы с параметром запроса company = id компании
Ayyappa
1
@ Мохамад Если вы думаете, что другой способ проще как в понимании, так и в применении ограничений, то не стесняйтесь дать ответ. Речь идет о том, чтобы сделать отображение явным в этом случае. Он может работать с параметром, но на самом деле вот в чем вопрос. Какой самый лучший способ.
Уэс
35

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

В более сложных системах может возникнуть соблазн предоставить URI, которые позволяют клиенту перемещаться по нескольким уровням отношений, например, /customers/1/orders/99/products.однако, этот уровень сложности может быть сложно поддерживать и он негибкий, если отношения между ресурсами изменятся в будущем. Вместо этого постарайтесь сделать URI относительно простыми . Как только у приложения есть ссылка на ресурс, должна быть возможность использовать эту ссылку для поиска элементов, связанных с этим ресурсом. Предыдущий запрос можно заменить на URI, /customers/1/ordersчтобы найти все заказы для клиента 1, а затем /orders/99/productsнайти продукты в этом заказе.

,

Наконечник

Избегайте использования URI ресурса более сложным, чем collection/item/collection.

Лонг Нгуен
источник
3
Ссылка, которую вы даете, удивительна вместе с тем, что вы не выделяете сложные URI.
Викко
Поэтому, когда я хочу создать команду для пользователя, это должны быть POST / команды (userId в теле) или POST / users /: id /
team
@coinhndp Привет, вы должны использовать POST / команды, и вы можете получить идентификатор пользователя после авторизации токена доступа. Я имею в виду, когда вы создаете материал, вам нужен код авторизации, верно? Я не знаю, какую платформу вы используете, но я уверен, что вы можете получить userId в контроллере API. Например: в ASP.NET API вызовите RequestContext.Principal из метода ApiController. В Spring Secirity вам поможет SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal (). В AWS NodeJS Lambda это cognito: имя пользователя в объекте заголовков.
Длинный Нгуен
Так что не так с POST / users /: id / команды. Я думаю, что это рекомендуется в документе Microsoft, который вы разместили выше
coinhndp
@coinhndp Если вы создаете команду как администратор, это хорошо. Но, как обычные пользователи, я не знаю, зачем вам нужен userId в пути? Я предполагаю, что у нас есть user_A и user_B, что вы думаете, если user_A может создать новую команду для user_B, если user_A вызовет POST / users / user_B / команды. Таким образом, нет необходимости передавать userId в этом случае, userId может получить после авторизации. Но команды /: id / projects - это хорошо, например, для установления связи между командой и проектом.
Длинный Нгуен
10

То, как выглядят ваши URL, не имеет ничего общего с REST. Все идет. На самом деле это «деталь реализации». Так же, как вы называете свои переменные. Все они должны быть уникальными и долговечными.

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

Почему так ? Насколько я знаю, API "RESTful" должен быть доступен для просмотра (вы знаете ... "Гипермедиа как движок состояния приложения"), поэтому клиент API не заботится о том, на что похожи ваши URL, пока они действительный (нет SEO, нет человека, который должен читать эти "дружеские ссылки", кроме как для отладки ...)

То, насколько приятный / понятный URL-адрес в REST API, интересует вас, как разработчика API, а не клиента API, как было бы имя переменной в вашем коде.

Самое главное, чтобы ваш клиент API знал, как интерпретировать ваш тип мультимедиа. Например, он знает, что:

  • Ваш тип мультимедиа имеет свойство links, в котором перечислены доступные / связанные ссылки.
  • Каждая ссылка идентифицируется отношением (точно так же, как браузеры знают, что ссылка [rel = "stylesheet"] означает, что это таблица стилей, или rel = favico - это ссылка на favicon ...)
  • и он знает, что означают эти отношения («компании» означают список компаний, «поиск» означает шаблонный URL для поиска по списку ресурса, «отделы» означают отделы текущего ресурса)

Ниже приведен пример обмена HTTP (тела в yaml, так как их легче написать):

Запрос

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Ответ: список ссылок на основной ресурс (компании, люди, что угодно ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Запрос: ссылка на компании (используя body.links.companies предыдущего ответа)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

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

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Таким образом, как вы видите, если вы идете по пути ссылок / отношений, то, как вы структурируете часть пути ваших URL, не имеет никакого значения для вашего клиента API. И если вы сообщаете клиенту структуру ваших URL-адресов в качестве документации, то вы не выполняете REST (или, по крайней мере, не уровень 3 в соответствии с « моделью зрелости Ричардсона »)

redben
источник
7
«Насколько приятен / понятен URL-адрес в REST API, вам интересно только как разработчику API, а не как клиенту API, как было бы имя переменной в вашем коде». Почему это НЕ будет интересно? Это очень важно, если кто-то, кроме вас, также использует API. Это часть пользовательского опыта, поэтому я бы сказал, что очень важно, чтобы это было легко понять разработчикам клиента API. Делать вещи еще проще для понимания, четко связывая ресурсы, - это, конечно, бонус (уровень 3 в предоставленном вами URL). Все должно быть интуитивно понятно и логично с четкими отношениями.
Иоаким
1
@Joakim Если вы создаете API уровня отдыха 3 (Гипертекст в качестве движка состояния приложения), то структура пути URL-адреса абсолютно не интересует клиента (пока он действителен). Если вы не стремитесь к уровню 3, то да, это важно и должно быть предположительно. Но настоящий REST - это уровень 3. Хорошая статья: martinfowler.com/articles/richardsonMaturityModel.html
redben
4
Я возражаю против того, чтобы когда-либо создавать API или пользовательский интерфейс, который не удобен для человека. Уровень 3 или нет, я согласен, что связывание ресурсов - отличная идея. Но предложить сделать так, чтобы «можно было изменить схему URL», значит не понимать реальности и того, как люди используют API. Так что это плохая рекомендация. Но уверен, что в лучшем из всех миров каждый будет на уровне 3 ОТДЫХ. Я включаю гиперссылки И использую понятную человеку схему URL. Уровень 3 не исключает первого, и один ДОЛЖЕН по моему мнению. Хорошая статья, хотя :)
Иоаким
Конечно, следует позаботиться о ремонтопригодности и других проблемах. Думаю, вы упустили смысл моего ответа: то, как выглядит URL-адрес, не заслуживает больших размышлений, и вы должны «просто сделать выбор и придерживаться его / быть». последовательный ", как я сказал в ответе. А в случае REST API, по крайней мере, на мой взгляд, удобство использования не в URL, а в основном в (типе носителя). В любом случае, я надеюсь, вы понимаете мою точку зрения :)
redben
9

Я не согласен с таким путем

GET /companies/{companyId}/departments

Если вы хотите получить отделы, я думаю, что лучше использовать ресурс / отделы

GET /departments?companyId=123

Я предполагаю, что у вас есть companiesтаблица и таблица, а departmentsзатем классы для отображения их на языке программирования, который вы используете. Я также предполагаю, что отделы могут быть присоединены к другим объектам, а не к компаниям, поэтому ресурс / отделы прост, удобно сопоставлять ресурсы с таблицами, а также вам не нужно столько конечных точек, так как вы можете повторно использовать

GET /departments?companyId=123

для любого вида поиска, например

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Если вы хотите создать отдел,

POST /departments

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

Максим Лаваль
источник
1
Для меня это приемлемый подход, только если вложенный объект имеет смысл как атомарный объект. Если это не так, не имеет смысла разбивать их на части.
Симме
Это то, что я сказал, если вы также хотите иметь возможность получать отделы, то есть, если вы будете использовать конечную точку / отделов.
Максим Лаваль
2
Может также иметь смысл разрешить включение отделов посредством отложенной загрузки при извлечении компании, например GET /companies/{companyId}?include=departments, поскольку это позволяет извлекать и компанию, и ее отделы в одном HTTP-запросе. Фрактал делает это действительно хорошо.
Мэтью Дейли
1
Когда вы настраиваете acls, вы, вероятно, захотите ограничить доступ к /departmentsконечной точке только для администратора, и каждая компания будет иметь доступ к своим собственным отделам только через `/ companies / {companyId} / департаменты`
Cuzox
@MatthewDaly OData также прекрасно справляется с этим благодаря $ expand
Роб Грант
1

Rails предоставляет решение этой проблемы: мелкое вложение .

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

partydrone
источник