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


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

Я могу придумать пару способов. Во-первых, у меня может быть каждый объект содержит список другого, поэтому объект игрока будет иметь список команд, к которым он принадлежит, и каждый объект команды список игроков, входящих в ее состав. Поэтому, чтобы добавить игрока в команду, вы просто разместите представление игрока в конечной точке, что-то вроде POST /player или после /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. Чтобы добавить игрока в команду, отправьте сообщение /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 229

7 ответов:

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

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

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

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

  1. REST - это создание эволюционирующих систем, если ничего другого. В данный момент вас может волновать только то, что данный игрок находится в данной команде, но в какой-то момент в будущем вы будет хочу прокомментировать эти отношения с большим количеством данных: как долго они были в этой команде, кто направил их в эту команду, кто их тренер/был в этой команде и т. д. и т. д.
  2. отдых зависит от кэширования для эффективность, которая требует некоторого рассмотрения для атомарности кэша и недействительности. Если вы разместите новый объект в /teams/3/players/ этот список будет признан недействительным, но вы не хотите альтернативный URL /players/5/teams/ чтобы оставаться в кэше. Да, разные кэши будут иметь копии каждого списка с разным возрастом, и мы не можем много сделать с этим, но мы можем по крайней мере минимизировать путаницу для пользователя, публикующего обновление, ограничивая количество объектов, которые нам нужно аннулировать в локальном кэше их клиента, чтобы один и только один at /memberships/98745 (обсуждение см. Хелланд о "альтернативные показатели" в жизнь вне распределенных транзакций для более детального обсуждения).
  3. вы можете реализовать вышеуказанные 2 пункта, просто выбрав /players/5/teams или /teams/3/players (но не оба). Давайте предположим первое. В какой-то момент, однако, вы захотите зарезервировать /players/5/teams/ список настоящее членство, и все же иметь возможность ссылаться на мимо членство где-то. Сделай /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 до /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 s для Ids вместе.

это позволяет нам повторно запустить оба /playersPUT и /membershipsPUT без последствия:

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 ресурсы, это точно такой же ресурс, который вы получаете при вызове GET /Teams/{teamId}. Обратное следует тому же принципу, вы получаете массив Players ресурсы при вызове GET /Teams/{teamId}/Players.

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

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

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

The 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 просты и понятны.

всякий раз, когда a связь создается, обновляется или удаляется, как 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. / игроки (это главный ресурс)
  2. / teams / {id} / players (это ресурс отношений, поэтому он реагирует иначе, чем 1)
  3. /членство (это отношения, но семантически сложные)
  4. /игроки/членство (это отношения, но семантически сложные)

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