Как обрабатывать отношения "многие ко многим" в RESTful API?

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

Я могу придумать пару способов. Во-первых, каждый объект может содержать список другого, поэтому объект игрока будет иметь список команд, которым он принадлежит, и каждый объект команды список игроков, входящих в ее состав. Поэтому, чтобы добавить игрока в команду, вы просто разместите представление игрока в конечной точке, что-то вроде POST /player или POST /team С соответствующим объектом в качестве полезной нагрузки запроса. Это кажется мне самым "спокойным", но чувствует себя немного странно.

/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'        
]

какова наилучшая практика для этого?

7 ответов


в интерфейсе RESTful вы можете возвращать документы, описывающие отношения между ресурсами, кодируя эти отношения как ссылки. Таким образом, можно сказать, что команда имеет ресурс документа (/team/{id}/players) Это список ссылок на игроков (/player/{id}) в команде, и игрок может иметь ресурс документа (/player/{id}/teams) это список ссылок на команды, членом которых является игрок. Красиво и симметрично. Вы можете операциях по карте в этом списке достаточно легко, даже в отношениях собственные идентификаторы (возможно, у них было бы два идентификатора, в зависимости от того, думаете ли вы о команде отношений-первый или игрок-первый), если это облегчает дело. Единственный сложный бит заключается в том, что вы должны помнить, чтобы удалить отношения с другого конца, а также, если вы удалите его с одного конца, но строго обрабатывать это с помощью базовой модели данных, а затем иметь интерфейс REST быть представление этой модели будет сделать это проще.

идентификаторы отношений, вероятно, должны быть на основе UUIDs или что-то одинаково длинное и случайное, независимо от того, какой тип идентификаторов вы используете для команд и игроков. Это позволит вам использовать тот же UUID, что и компонент ID для каждого конца отношения, не беспокоясь о столкновениях (малые целые числа do не иметь преимущество). Если эти отношения членства имеют какие-либо свойства, отличные от простого факта, что они связывают игрока и команду двунаправленным образом, они должны иметь свою собственную идентичность, которая независимо от игроков и команд; получить на игрока " team view (/player/{playerID}/teams/{teamID}) может затем выполнить перенаправление HTTP на двунаправленное представление (/memberships/{uuid}).

Я рекомендую писать ссылки в любых XML-документах, которые вы возвращаете (если вы, конечно, производите XML), используя XLink xlink:href атрибуты.


сделайте отдельный набор /memberships/ ресурсы.

  1. REST - это создание эволюционирующих систем, если ничего другого. В данный момент вас может волновать только то, что данный игрок находится в данной команде, но в какой-то момент в будущем вы будет хотите аннотировать эти отношения с дополнительными данными: как долго они были в этой команде, кто направил их в эту команду, кто их тренер/был в этой команде и т. д.
  2. REST зависит от кэширования для эффективность, которая требует некоторого рассмотрения атомарности кэша и недействительности. Если вы разместите новый объект в /teams/3/players/ этот список будет признан недействительным, но вы не хотите альтернативный URL /players/5/teams/ для кэширования. Да, разные кэши будут иметь копии каждого списка с разными возрастами, и мы мало что можем с этим поделать, но мы можем по крайней мере минимизировать путаницу для пользователя, публикующего обновление, ограничивая количество объектов, которые нам нужно аннулировать в локальном кэше их клиента, чтобы один и только один at /memberships/98745 (обсуждение см. Хелланд о "альтернативные показатели" в жизнь за пределами распределенных транзакций для более детального обсуждения).
  3. вы можете реализовать вышеуказанные 2 пункта, просто выбрав /players/5/teams или /teams/3/players (но не оба). Предположим первое. В какой-то момент, однако, вы захотите зарезервировать /players/5/teams/ список настоящее членство, и все же иметь возможность ссылаться на мимо членство где-то. Make /players/5/memberships/ список гиперссылок на /memberships/{id}/ ресурсы, а затем вы можете добавить /players/5/past_memberships/ когда вам нравится, без необходимости разбивать закладки каждого для отдельных ресурсов членства. Это общая концепция; я уверен, что вы можете представить себе другие подобные фьючерсы, которые более применимы к вашему конкретному случаю.

Я бы карту таких отношений с суб-ресурсов, общий дизайн/обхода будет иметь следующий вид:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

# teams/players subresource
/teams/{teamId}/players/{playerId}

в Restful-терминах это помогает много не думать о 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 игроков и команд.


существующие ответы не объяснить роли последовательности и идемпотентности-которые мотивируют их рекомендации UUIDs/случайные числа для идентификаторов и PUT вместо POST.

если мы рассмотрим случай, когда у нас есть простой сценарий типа "добавить нового игрока в команду" мы столкнулись с проблемами непротиворечивости.

поскольку игрок не существует, нам нужно:

POST /players { "Name": "Murray" } //=> 302 /players/5
POST /teams/1/players/5

однако, должен клиент сбой операции после POST to /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 идемпотентна

чтобы операция была идемпотентной, она должна исключать внешние данные, такие как последовательности id, созданные сервером. Вот почему люди рекомендуют оба PUT и UUIDs для Ids вместе.

это позволяет нам повтор как /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

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

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


мое предпочтительное решение-создать три ресурса:Players, Teams и TeamsPlayers.

Итак, чтобы получить всех игроков команды, просто перейдите в Teams ресурс и получить всех своих игроков, позвонив GET /Teams/{teamId}/Players.

с другой стороны, чтобы получить все команды игрок сыграл, получить Teams ресурсов Players. Звоните GET /Players/{playerId}/Teams.

и, чтобы получить много-ко-многим отношения вызова GET /Players/{playerId}/TeamsPlayers или GET /Teams/{teamId}/TeamsPlayers.

обратите внимание, что в этом решение, когда вы звоните GET /Players/{playerId}/Teams вы получите массив Teams resources, это точно такой же ресурс, который вы получаете при вызове GET /Teams/{teamId}. Обратное следует тому же принципу, вы получаете массив Players ресурсы при вызове GET /Teams/{teamId}/Players.

в обоих вызовах информация об отношениях не возвращается. Например, нет contractStartDate возвращается, потому что возвращаемый ресурс не имеет информации о связи, только о своем собственном ресурсе.

для того чтобы общаться с n-n отношения, позвоните либо GET /Players/{playerId}/TeamsPlayers или GET /Teams/{teamId}/TeamsPlayers. Эти вызовы возвращают именно ресурс,TeamsPlayers.

этой TeamsPlayers ресурс id, playerId, teamId атрибуты, а также некоторые другие, чтобы описать отношения. Кроме того, у него есть методы, необходимые для борьбы с ними. GET, POST, PUT, DELETE и т. д., которые будут возвращать, включать, обновлять, удалять ресурс отношений.

на TeamsPlayers ресурс реализует некоторые запросы, как GET /TeamsPlayers?player={playerId} вернуть все TeamsPlayers отношения игрок идентифицирован {playerId} есть. Следуя той же идее, используйте GET /TeamsPlayers?team={teamId} вернуть все TeamsPlayers, которые играли в {teamId} команда. В любом GET вызова, ресурс TeamsPlayers возвращается. Возвращаются все данные, связанные с отношением.

при вызове GET /Players/{playerId}/Teams (или GET /Teams/{teamId}/Players), ресурс Players (или Teams) называет TeamsPlayers для возврата связанных команд (или игроков) с помощью фильтра запросов.

GET /Players/{playerId}/Teams работает это:

  1. найти все TeamsPlayers что плеер и id = playerId. (GET /TeamsPlayers?player={playerId})
  2. цикл возвращаемого TeamsPlayers
  3. с помощью teamId полученные от TeamsPlayers, называют GET /Teams/{teamId} и хранить возвращенные данные
  4. после завершения цикла. Верните все команды, которые попали в петлю.

вы можете использовать тот же алгоритм, чтобы получить всех игроков из команды, при вызове GET /Teams/{teamId}/Players, но обмен командами и игроками.

мои ресурсы будут выглядеть так:

/api/Teams/1:
{
    id: 1
    name: 'Vasco da Gama',
    logo: '/img/Vascao.png',
}

/api/Players/10:
{
    id: 10,
    name: 'Roberto Dinamite',
    birth: '1954-04-13T00:00:00Z',
}

/api/TeamsPlayers/100
{
    id: 100,
    playerId: 10,
    teamId: 1,
    contractStartDate: '1971-11-25T00:00:00Z',
}

это решение зависит только от ресурсов REST. Хотя некоторые дополнительные вызовы могут быть необходимы для получения данных от игроков, команд или их отношений, все методы HTTP легко реализуются. POST, PUT, DELETE просты и понятны.

всякий раз, когда связь создается, обновляется или удаляется, как Players и Teams ресурсы обновляются автоматически.


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

скажем, для 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"
        },
        ...
    ]
}

  1. / players (является главным ресурсом)
  2. / teams / {id} / players (является ресурсом отношений, поэтому он реагирует по-разному, что 1)
  3. / членство (является отношением, но семантически сложным)
  4. /игроки/членство (это отношения, но семантически сложные)

Я предпочитаю 2