Как обрабатывать отношения "многие ко многим" в RESTful API?
Представьте, что у вас есть 2 объекта, Игрок и Команда, где игроки могут быть в нескольких командах. В моей модели данных у меня есть таблица для каждого объекта и таблица соединений для поддержания отношений. Hibernate отлично справляется с этим, но как я могу разоблачить эти отношения в RESTful API?
Я могу придумать пару способов. Во-первых, у меня может быть каждый объект, содержащий список другого, поэтому у объекта Player будет список команд, к которому он принадлежит, и каждый объект команды имеет список принадлежащих ему Игроков. Поэтому, чтобы добавить игрока в команду, вы просто установите POST-представление игрока в конечную точку, что-то вроде POST /player
или POST /team
с соответствующим объектом в качестве полезной нагрузки запроса. Это кажется мне самым "RESTful", но кажется немного странным.
/api/team/0:
{
name: 'Boston Celtics',
logo: '/img/Celtics.png',
players: [
'/api/player/20',
'/api/player/5',
'/api/player/34'
]
}
/api/player/20:
{
pk: 20,
name: 'Ray Allen',
birth: '1975-07-20T02:00:00Z',
team: '/api/team/0'
}
Другим способом, я могу думать, чтобы сделать это, было бы разоблачить отношения как ресурс в своем собственном праве. Итак, чтобы просмотреть список всех игроков в данной команде, вы можете сделать GET /playerteam/team/{id}
или что-то в этом роде и вернуть список объектов PlayerTeam. Чтобы добавить игрока в команду, POST /playerteam
с соответствующим образом созданным объектом PlayerTeam в качестве полезной нагрузки.
/api/team/0:
{
name: 'Boston Celtics',
logo: '/img/Celtics.png'
}
/api/player/20:
{
pk: 20,
name: 'Ray Allen',
birth: '1975-07-20T02:00:00Z',
team: '/api/team/0'
}
/api/player/team/0/:
[
'/api/player/20',
'/api/player/5',
'/api/player/34'
]
Какова наилучшая практика для этого?
Ответы
Ответ 1
В интерфейсе RESTful вы можете возвращать документы, описывающие отношения между ресурсами, кодируя эти отношения как ссылки. Таким образом, можно сказать, что у команды есть ресурс документа (/team/{id}/players
), который является списком ссылок на игроков (/player/{id}
) в команде, и у игрока может быть ресурс документа (/player/{id}/teams
), который является список ссылок на команды, в которых участвует игрок. Хороший и симметричный. Вы можете выполнять операции с картами в этом списке достаточно легко, даже если у вас есть отношения с его собственными идентификаторами (возможно, у них будет два идентификатора, в зависимости от того, думаете ли вы о команде отношений - сначала или о первом игроке), если это упростит ситуацию, Единственный сложный бит в том, что вы должны помнить о том, чтобы удалить отношения с другого конца, а также, если вы удалите его с одного конца, но строго справляетесь с этим, используя базовую модель данных, а затем, имея интерфейс REST, эта модель сделает это проще.
Идентификаторы отношений, вероятно, должны основываться на UUID или на чем-то одинаковом длительном и случайном, независимо от того, какой тип идентификаторов вы используете для команд и игроков. Это позволит вам использовать тот же UUID, что и компонент ID для каждого конца отношений, не беспокоясь о столкновениях (маленькие целые числа не имеют такого преимущества). Если эти членские отношения имеют какие-либо свойства, отличные от голого факта, что они связаны с игроком и командой в двунаправленном режиме, у них должна быть своя собственная идентичность, которая не зависит от игроков и команд; GET в представлении команды игрока "(/player/{playerID}/teams/{teamID}
) может затем перенаправить HTTP в двунаправленный вид (/memberships/{uuid}
).
Я рекомендую писать ссылки в любых возвращаемых вами XML-документах (если вы, разумеется, создаете XML), используя XLink xlink:href
атрибуты.
Ответ 2
Создайте отдельный набор ресурсов /memberships/
.
- REST - это создание эволюционирующих систем, если ничего другого. На данный момент вам может быть только интересно, что данный игрок находится в данной команде, но в какой-то момент в будущем вам захочется комментировать эти отношения с большим количеством данных: как долго они были в этой команде, кто их отсылал к той команде, кто их тренер/находился в то время в этой команде и т.д. и т.д.
- REST зависит от кэширования для эффективности, что требует некоторого учета атомарности и аннулирования кеша. Если вы POST нового объекта в
/teams/3/players/
, этот список будет признан недействительным, но вы не хотите, чтобы альтернативный URL /players/5/teams/
оставался кешированным. Да, разные кеши будут иметь копии каждого списка с разным возрастом, и мы мало что можем с этим поделать, но мы можем, по крайней мере, свести к минимуму путаницу для пользователя POST'ing обновления, ограничив количество объектов, которые нам нужны, чтобы аннулировать в локальном кеше своего клиента к одному и только одному в /memberships/98745
(см. обсуждение Helland "альтернативных индексов" в Жизнь за пределами распределенных транзакций для более подробное обсуждение).
- Вы можете реализовать вышеуказанные 2 балла, просто выбрав
/players/5/teams
или /teams/3/players
(но не оба). Предположим, что первое. Однако в какой-то момент вы захотите зарезервировать /players/5/teams/
список текущих членских составов и, тем не менее, сможете где-то ссылаться на прошлые членства. Сделайте /players/5/memberships/
список гиперссылок на ресурсы /memberships/{id}/
, а затем вы можете добавить /players/5/past_memberships/
, когда захотите, без необходимости разбивать все закладки для отдельных ресурсов членства. Это общая концепция; Я уверен, вы можете представить себе другие подобные фьючерсы, которые более применимы к вашему конкретному случаю.
Ответ 3
Я бы сопоставлял такие отношения с субресурсами, общий дизайн/обход был бы следующим:
# team resource
/teams/{teamId}
# players resource
/players/{playerId}
# teams/players subresource
/teams/{teamId}/players/{playerId}
В Restful-terms это помогает многому не думать о SQL и присоединяется, но больше в коллекциях, под-коллекциях и обходах.
Некоторые примеры:
# getting player 3 who is on team 1
# or simply checking whether player 3 is on that team (200 vs. 404)
GET /teams/1/players/3
# getting player 3 who is also on team 3
GET /teams/3/players/3
# adding player 3 also to team 2
PUT /teams/2/players/3
# getting all teams of player 3
GET /players/3/teams
# withdraw player 3 from team 1 (appeared drunk before match)
DELETE /teams/1/players/3
# team 1 found a replacement, who is not registered in league yet
POST /players
# from payload you get back the id, now place it officially to team 1
PUT /teams/1/players/44
Как вы видите, я не использую POST для размещения игроков в командах, но PUT, что улучшает отношения между игроками и командами n: n.
Ответ 4
Существующие ответы не объясняют роли последовательности и идемпотентности, которые мотивируют свои рекомендации UUIDs
/случайных чисел для идентификаторов и PUT
вместо POST
.
Если мы рассмотрим случай, когда у нас есть простой сценарий, например "Добавить нового игрока в команду", мы сталкиваемся с проблемами согласованности.
Поскольку игрок не существует, нам необходимо:
POST /players { "Name": "Murray" } //=> 302 /players/5
POST /teams/1/players/5
Однако, если операция клиента завершится с ошибкой после POST
до /players
, мы создали игрока, который не принадлежит команде:
POST /players { "Name": "Murray" } //=> 302 /players/5
// *client failure*
// *client retries naively*
POST /players { "Name": "Murray" } //=> 302 /players/6
POST /teams/1/players/6
Теперь у нас есть сиротский дублированный игрок в /players/5
.
Чтобы исправить это, мы можем написать специальный код восстановления, который проверяет наличие сиротских игроков, которые соответствуют некоторому естественному ключу (например, Name
). Это настраиваемый код, который необходимо протестировать, стоит больше денег и времени и т.д. И т.д.
Чтобы избежать необходимости использования специального кода восстановления, мы можем реализовать PUT
вместо POST
.
Из RFC:
Цель PUT
- идемпотент
Для того чтобы операция была идемпотентной, ей необходимо исключить внешние данные, такие как серверные последовательности идентификаторов. Вот почему люди рекомендуют вместе PUT
и UUID
для Id
вместе.
Это позволяет нам повторно запускать как /players
PUT
, так и /memberships
PUT
без последствий:
PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
// *client failure*
// *client YOLOs*
PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
PUT /teams/1/players/23lkrjrqwlej
Все в порядке, и нам не нужно было ничего делать, кроме повторения попыток частичных сбоев.
Это больше добавление к существующим ответам, но я надеюсь, что это ставит их в контексте большей картины того, насколько гибким и надежным может быть ReST.
Ответ 5
Я знаю, что есть ответ, отмеченный как принятый для этого вопроса, однако, вот как мы могли бы решить ранее поднятые проблемы:
Скажем, для PUT
PUT /membership/{collection}/{instance}/{collection}/{instance}/
В качестве примера следующие действия будут приводить к одному и тому же эффекту без необходимости синхронизации, поскольку они выполняются на одном ресурсе:
PUT /membership/teams/team1/players/player1/
PUT /membership/players/player1/teams/team1/
теперь, если мы хотим обновить несколько членств для одной команды, мы могли бы сделать следующее (с правильной проверкой):
PUT /membership/teams/team1/
{
membership: [
{
teamId: "team1"
playerId: "player1"
},
{
teamId: "team1"
playerId: "player2"
},
...
]
}
Ответ 6
- /players (является основным ресурсом)
- /teams/{id}/players (это ресурс отношений, поэтому он реагирует по-разному, что 1)
- /membershiphips (это отношения, но семантически сложные)
- /players/membershiphips (это отношения, но семантически сложные)
Я предпочитаю 2