Как обрабатывать отношения «многие ко многим» в RESTful API? - PullRequest
259 голосов
/ 13 июня 2011

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

Я могу придумать пару способов. Во-первых, у меня может быть каждый объект, содержащий список другого, поэтому у объекта Player будет список Команд, к которым он принадлежит, а у каждого объекта Team будет список принадлежащих ему Игроков. Таким образом, чтобы добавить игрока в команду, вы просто поместите представление игрока в конечную точку, что-то вроде 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 ]

240 голосов
/ 13 июня 2011

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

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

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

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

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

56 голосов
/ 13 июня 2011

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

# 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 отношения игроков и команд.

17 голосов
/ 06 января 2017

Существующие ответы не объясняют роли последовательности и идемпотентности - которые мотивируют их рекомендации 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 голосов
/ 08 августа 2018

Мое предпочтительное решение - создать три ресурса: 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 не возвращается, поскольку возвращаемый ресурс не имеет информации об отношении, а только о своем собственном ресурсе.

Чтобы справиться с отношением nn, вызовите 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 автоматически обновляются.

1 голос
/ 09 июля 2016

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

Допустим, для 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"
        },
        ...
    ]
}
0 голосов
/ 21 июня 2014
  1. / Players (является основным ресурсом)
  2. / команды / {id} / Players (это ресурс отношений, поэтому он реагирует иначе, чем 1)
  3. / членство (это отношения, но семантически сложные)
  4. / игроки / членство (это отношения, но семантически сложные)

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

...