Дизайн API REST: вложенная коллекция против нового корня

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

чтобы продемонстрировать концепцию, предположим, у меня есть коллекции City, Business и Employees. Типичный API может быть построен следующим образом. Представьте, что ABC, X7N и WWW являются ключами, например GUID:

GET Api/City/ABC/Businesses                       (returns all Businesses in City ABC)
GET Api/City/ABC/Businesses/X7N                   (returns business X7N)
GET Api/City/ABC/Businesses/X7N/Employees         (returns all employees at business X7N)
PUT Api/City/ABC/Businesses/X7N/Employees/WWW     (updates employee WWW)

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

вот что должен уметь делать потребитель API:

  • получить бизнес в городе (GET Api/City/ABC/Businesses)
  • получить всех сотрудников в бизнесе (GET Api/City/ABC/Businesses/X7N/Employees)
  • обновить информацию об отдельных сотрудниках (PUT Api/City/ABC/Businesses/X7N/Employees/WWW)

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

  • чтобы получить сотрудников в бизнес, единственным необходимым параметром является ключ бизнеса (X7N).
  • для обновления отдельного сотрудника, единственный параметр, необходимый ему ключ сотрудника (WWW)

ничто в коде бэкэнда не требует неключевой информации для поиска бизнеса или обновления сотрудника. Вместо этого появляются следующие конечные точки лучше:

GET Api/City/ABC/Businesses                       (returns all Businesses in City ABC)
GET Api/Businesses/X7N                            (returns business X7N)
GET Api/Businesses/X7N/Employees                  (returns all employees at business X7N)
PUT Api/Employees/WWW                             (updates employee WWW)

как вы можете видеть, я создал новый root для предприятий и сотрудников, хотя с точки зрения домена они являются суб/суб-суб-коллекцией.

ни одно решение не кажется мне очень чистым.

  • первый пример запрашивает ненужную информацию, но структурируется таким образом, что кажется "естественным" потребителю (отдельные элементы из коллекции извлекаются через нижние листья)
  • в второй пример запрашивает только необходимую информацию, но не структурирован "естественным" способом - подколлекции доступны через корни
  • отдельный корень сотрудника не будет работать при добавлении нового сотрудника, так как нам нужно знать, в какой бизнес добавить сотрудника, а это означает, что вызов, по крайней мере, должен находиться под корнем бизнеса, например POST Api/Businesses/X7N7/Employees, что делает все еще более запутанным.

есть ли чище, третий способ, которым я не являюсь думаешь?

5 ответов


Я не вижу, как REST добавляет ограничение, что два ресурса не могут иметь одинаковое значение. The resourceType/ID - Это просто пример самого простого варианта использования, а не лучший способ пойти с точки зрения покоя.

если вы читали пункт 5.2.1.1 диссертации Роя Филдинга тщательно, вы заметите, что Филдинг делает disctinction между стоимостью и ресурс. Теперь ресурс должен иметь уникальный URI, это правда. Но ничто не мешает двум ресурсам иметь одинаковое значение:

например, "предпочтительной версией авторов" академической статьи является сопоставление, значение которого изменяется со временем, в то время как сопоставление с "статьей, опубликованной в трудах конференции X", является статическим. Это два различных ресурса,даже если они оба сопоставляются с одним и тем же значением в какой-то момент времени. различие необходимо для того, чтобы оба ресурса можно было идентифицировать и ссылаться независимо. Аналогичный пример из software engineering-отдельная идентификация файла исходного кода, контролируемого версией, при обращении к "последней редакции", "номеру версии 1.2.7" или "редакции, включенной в выпуск Orange."

Итак, ничто не мешает вам, как вы говорите, изменить корень. В вашем примере Business - это ценность, а не ресурс. Совершенно спокойно создать ресурс, который представляет собой список " каждого бизнеса, расположенного в городе "(просто как пример Роя, "изменения, включенные в выпуск Orange"), имея при этом ресурс "business which ID is x" (например, "revision number x").

на Employees, Я хотел бы сохранить API/Businesses/X7N/Employees как отношение между бизнесом и его сотрудниками является состав отношения, и таким образом, как вы говорите,Employees может и должен быть доступен только через Businesses корневой класс. Но это не требование отдыха, а другая альтернатива совершенно спокойна, поскольку что ж.


отметим, что это идет в паре с применением принципа HATEAOS. В вашем API список предприятий, расположенных в городе, может (и, возможно, должен с теоретической точки зрения) быть просто списком ссылок на API/Businesses. Но это означало бы, что клиенты должны были бы сделать один круговой переход к серверу для каждого из элементов в списке. Это не эффективно, и, чтобы оставаться прагматичным, то, что я делаю, - это встроить представление бизнес в списке вместе с self ссылка на URI, который был бы в этом примере API/Businesses.


вы не должны путать REST с применением определенного соглашения об именовании URI.

Как называются ресурсы, является полностью вторичным. Вы пытаетесь использовать Соглашения об именах ресурсов HTTP - это не имеет ничего общего с REST. Сам Рой Филдинг неоднократно заявлял об этом в документах, приведенных выше другими. REST-это не протокол, это архитектурный стиль.

фактически, Рой Филдинг заявляет в своем комментарии к блогу 2008 года (http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 6/20/2012):

"API REST не должен определять фиксированные имена ресурсов или иерархии (очевидная связь клиент и сервер). Серверы должны иметь свободу управления собственным пространством имен. Вместо, разрешить серверам инструктировать клиентов о том, как создавать соответствующие URI, например, в HTML-формы и шаблоны URI, определяя эти инструкции в типах носителей и ссылке родство."

Так, в сущности:

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

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

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

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

ближе к делу:

единственные реальные ограничения REST у вас есть, если вы уже решили использовать HTTP с GET Ставят и так далее, стоят:

    1. ты не будешь presumeth какого-либо предварительного ("out of band") знание между клиентом и серверами. *

посмотрите на свое предложение №1 выше в этом свете. Вы предполагаете, что клиенты знают ключи для городов, которые содержатся в вашей системе? Неправильно - это не успокаивает. Таким образом, сервер должен дать список городов как список вариантов в некотором роде. Так ты собираешься перечислить все города мира? Я думаю, нет, но тогда вам придется поработать над тем, как вы планируете это сделать, что подводит нас к:

    1. API REST должен потратить почти все свои описательные усилия на определение типов носителей, используемых для представления ресурсов и управления состоянием приложения ...

Я думаю, что чтение упомянутого блога Роя Филдинга поможет вам значительно.


в дизайне URL RESTful-API должно быть совершенно неважно - или, по крайней мере, побочная проблема, поскольку обнаруживаемость кодируется в гипертексте, а не в URL-пути. посмотрите на ресурсы, связанные в REST tag wiki здесь, на StackOverflow.

но если вы хотите создать удобочитаемые URL-адреса для вашего UC, я бы предложил следующее:

  1. используйте тип ресурса, который вы создаете / обновляете / запрашиваете как первая часть URL (после вашего префикса API). Поэтому, когда кто-то видит URL, он сразу знает, на какие ресурсы указывает этот URL. GET /Api/Employees... - единственный способ получить ресурсы сотрудников из API.

  2. используйте уникальные идентификаторы для каждого ресурса независимо от отношений, в которых они находятся. Так что GET /Api/<CollectionType>/UniqueKey должно возвращать допустимое представление ресурса. Никто не должен беспокоиться о том, где находится сотрудник. (Но возвращенный сотрудник должен имейте связи с бизнесом (и для удобства ради города), к которому он принадлежит.) GET /Api/Employees/Z6W возвращает сотрудника с этим идентификатором независимо от того, где находится.

  3. если вы хотите получить определенный ресурс: поместите параметр запроса в конец (вместо этого в иерархическом порядке, описанном в вопросе). Вы можете использовать строку запроса URL (GET /Api/Employees?City=X7N) или выражение параметра матрицы (GET /Api/Employees;City=X7N;Business=A4X,A5Y). Это позволит вам легко выразить коллекцию Сотрудники в конкретном городе-независимо от бизнеса, в котором они находятся.

боковая узел:

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


третий способ, который я вижу, - это сделать корневые ресурсы предприятий и сотрудников и использовать параметры запроса для фильтрации коллекций:

GET Api/Businesses?city=ABC                       (returns all Businesses in City ABC)
GET Api/Businesses/X7N                            (returns business X7N)
GET Api/Employees?businesses=X7N                  (returns all employees at business X7N)
PUT Api/Employees/WWW                             (updates employee WWW)

ваши оба решения используют концепцию подресурсов REST, которая требует, чтобы подресурс был включен в родительский ресурс так:

GET Api/City/ABC/Businesses

в ответ следует также вернуть данные, предоставленные:

  GET Api/City/ABC/Businesses/X7N                 
  GET Api/City/ABC/Businesses/X7N/Employees 

похожие на:

GET Api/Businesses/X7N

который должен возвращать данные, предоставленные:

GET Api/Businesses/X7N/Employees

он будет сделать размер огромный и время, необходимое для генерации возрастет.

чтобы сделать REST API чистым, каждый ресурс должен иметь только один ограниченный URI, который находится ниже шаблонов:

 GET  /resources
 GET  /resources/{id}
 POST /resources
 PUT  /resources/{id}

Если вам нужно сделать ссылки между ресурсами, используйте HATEOAS


перейти к примеру 1. Я бы не стал беспокоиться о ненужной информации с точки зрения сервера. URL-адрес должен четко идентифицировать ресурс уникальным образом с точки зрения клиента. Если бы клиент не знал, что /Employee/12 означает, не зная сначала, что это на самом деле /Businesses/X7N/Employees/12 тогда первый URL кажется избыточным.

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