Как обрабатывать отношения "многие ко многим" в 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