Составной ресурс REST
Я столкнулся с проблемой на работе, где я не могу найти информацию о стандартном стандарте или практике для выполнения операций CRUD в веб-службе RESTful против ресурса, чей первичный ключ является составной частью других идентификаторов ресурсов. Мы используем MVC WebApi для создания контроллеров. Например, у нас есть три таблицы:
-
Product
: PK = ProductId
-
Part
: PK = PartId
-
ProductPartAssoc
: PK = (ProductId, PartId)
Продукт может иметь много частей, а часть может быть компонентом многих продуктов. Таблица ассоциации также содержит дополнительную информацию, относящуюся к самой ассоциации, которая должна быть доступна для редактирования.
У нас есть классы ProductsController
и PartsController
, которые обрабатывают обычные операции GET/PUT/POST/DELETE с использованием шаблонов маршрутов, определенных как: {controller}/{id}/{action}
, так что работают следующие IRI:
- GET, POST
/api/Products
- возвращает все продукты, создает новый продукт
- GET, PUT, DELETE
/api/Products/1
- извлекает/обновляет/удаляет продукт 1
- GET, POST
/api/Parts
- возвращает все части, создает новую часть
- GET, PUT, DELETE
/api/Parts/2
- извлекает/обновляет/удаляет часть 2
- GET
/api/Products/1/Parts
- получить все детали для продукта 1
- GET
/api/Parts/2/Products
- получить все продукты, для которых часть 2 является компонентом
В случае возникновения проблем возникает вопрос о том, как определить шаблон маршрута для ресурсов ProductPartAssoc. Каким должен быть шаблон маршрута и IRI для получения данных ассоциации?
Соблюдая соглашение, я бы ожидал чего-то вроде:
- GET, POST
/api/ProductPartAssoc
- возвращает все ассоциации, создает ассоциацию
- GET, PUT, DELETE
/api/ProductPartAssoc/[1,2]
- извлекает/обновляет/удаляет связь между продуктом 1 и частью 2
Мои коллеги находят это эстетически недовольным, хотя и, похоже, считают, что лучше не иметь класс ProductPartAssocController
, а скорее добавить дополнительные методы в ProductsController
для управления данными ассоциации:
- GET, PUT, DELETE
/api/Products/1/Parts/2
- получить данные для связи между продуктом 1 и частью 2, а не данными для части 2 в качестве члена части 1, что обычно было бы основано на других примерах, таких как /Book/5/Chapter/3
, что я видел в другом месте.
- POST Не знаю, чего ожидать от IRI. К сожалению, они являются лицами, принимающими решения.
В конце дня, я думаю, что я ищу, это либо проверка, либо направление, на которое я могу указать, и сказать: "Смотрите, это то, что делают другие люди".
Какова типичная практика для работы с ресурсами, идентифицированными составными ключами?
Ответы
Ответ 1
Мне тоже нравится эстетика /api/Products/1/Parts/2
. Вы также можете использовать несколько маршрутов для одного и того же действия, чтобы вы могли удвоиться, а также предложить /api/Parts/2/Products/1
в качестве альтернативного URL-адреса для того же ресурса.
Что касается POST, вы уже знаете составной ключ. Так почему бы не устранить необходимость в POST и просто использовать PUT для создания и обновления? POST для URL ресурса коллекции отлично, если ваша система генерирует первичный ключ, но в тех случаях, когда у вас есть составная часть уже известных первичных ключей, зачем вам нужен POST?
Тем не менее, мне также нравится идея иметь отдельный ProductPartAssocController
, чтобы содержать действия для этих URL. Вам нужно будет выполнить собственное сопоставление маршрутов, но если вы используете что-то вроде AttributeRouting.NET, это очень легко сделать.
Например, мы делаем это для управления пользователями в ролях:
PUT, GET, DELETE /api/users/1/roles/2
PUT, GET, DELETE /api/roles/2/users/1
6 URL, но всего 3 действия, все в GrantsController
(мы называем gerund между пользователями и ролями "Grant" ). Класс выглядит примерно так: AttributeRouting.NET:
[RoutePrefix("api")]
[Authorize(Roles = RoleName.RoleGrantors)]
public class GrantsController : ApiController
{
[PUT("users/{userId}/roles/{roleId}", ActionPrecedence = 1)]
[PUT("roles/{roleId}/users/{userId}", ActionPrecedence = 2)]
public HttpResponseMessage PutInRole(int userId, int roleId)
{
...
}
[DELETE("users/{userId}/roles/{roleId}", ActionPrecedence = 1)]
[DELETE("roles/{roleId}/users/{userId}", ActionPrecedence = 2)]
public HttpResponseMessage DeleteFromRole(int userId, int roleId)
{
...
}
...etc
}
Это кажется довольно интуитивным подходом ко мне. Сохранение действий в отдельном контроллере также делает более компактные контроллеры.
Ответ 2
Я предлагаю:
- POST
/api/PartsProductsAssoc
: создать ссылку между частью и продуктом. Включите идентификаторы частей и продуктов в данные POST.
- GET, PUT, DELETE
/api/PartsProductsAssoc/<assoc_id>
: ссылка для чтения/обновления/удаления с <assoc_id>
(не является идентификатором части или продукта, да, это означает создание нового столбца в вашей таблице PartsProductsAssoc).
- GET
/api/PartsProductsAssoc/Parts/<part_id>/Products
: получить список продуктов, связанных с данной частью.
- GET
/api/PartsProductsAssoc/Products/<product_id>/Parts
: получить список деталей, связанных с данным продуктом.
Причины такого подхода:
- Единый, полностью квалифицированный URI для каждой ссылки.
- Изменение ссылки изменяет один ресурс REST.
Для получения дополнительной информации см. https://www.youtube.com/watch?v=hdSrT4yjS1g в 56:30.