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


288

Представьте, что у вас есть 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'        
]

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

Ответы:


129

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

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

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


265

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

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

11
Пункты 1 и 2 прекрасно объяснены, спасибо, если у кого-то будет больше мяса для пункта 3 в реальной жизни, это помогло бы мне.
Алена

2
Лучший и самый простой ответ IMO спасибо! Наличие двух конечных точек и их синхронизация сопряжены с множеством осложнений.
Венкат Д.

7
привет фуманчу Вопросы: В остальной конечной точке / members / 98745 что означает это число в конце URL-адреса? Это уникальный идентификатор для членства? Как можно взаимодействовать с конечной точкой членства? Чтобы добавить игрока, будет ли отправлен POST, содержащий полезную нагрузку с {team: 3, player: 6}, создавая тем самым связь между ними? Как насчет GET? Вы бы отправили GET в / memberships? player = и / membersihps? team =, чтобы получить результаты? Это идея? Я что-то упустил? (Я пытаюсь выучить спокойные конечные точки). В таком случае действительно ли id 98745 в членстве / 98745 действительно полезен?
aruuuuu

@aruuuuu отдельная конечная точка для ассоциации должна быть обеспечена суррогатным ПК. Это также значительно облегчает жизнь: / memberships / {membersId}. Ключ (playerId, teamId) остается уникальным и, таким образом, может использоваться на ресурсах, которые имеют это отношение: / team / {teamId} / Players и / Players / {playerId} / команды. Но не всегда, когда такие отношения поддерживаются с обеих сторон. Например, Рецепты и Ингредиенты: вам вряд ли когда-нибудь понадобится использовать / ингридиенты / {ингридиент} / рецепты /.
Александр Паламарчук

65

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

# 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, который лучше обрабатывает ваши отношения игроков и команд.


20
Что, если team_player имеет дополнительную информацию, такую ​​как статус и т. Д.? где мы представляем это в вашей модели? можем ли мы продвинуть его на ресурс и предоставить URL-адреса для него, как для игры /, игрока /
Нарендра Камма

Эй, быстрый вопрос только для того, чтобы убедиться, что я правильно понял: GET / groups / 1 / Players / 3 возвращает пустое тело ответа. Единственный значимый ответ от этого - 200 против 404. Информация об объекте игрока (имя, возраст и т. Д.) НЕ возвращается GET / team / 1 / Players / 3. Если клиент хочет получить дополнительную информацию об игроке, он должен GET / Players / 3. Это все правильно?
Вердагон

2
Я согласен с Вашим отображением, но у меня есть один вопрос. Это вопрос личного мнения, но что вы думаете о POST / команды / 1 / игроков и почему вы не используете его? Видите ли вы какие-либо недостатки / заблуждение в этом подходе?
Якуб Кнейзлик

2
POST не идемпотентен, т. Е. Если вы выполняете POST / команды / 1 / игроков n раз, вы измените n-раз / команды / 1. но перемещение игрока в / команды / 1 n раз не изменит состояние команды, поэтому использование PUT более очевидно.
Мануэль Алдана

1
@NarendraKamma Я полагаю, просто отправить statusкак параметр в запросе PUT? Есть ли обратная сторона этого подхода?
Traxo

22

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

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

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

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

Однако, если клиентская операция завершится неудачно после POSTto /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с для 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

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

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


В этой гипотетической конечной точке, откуда вы взяли 23lkrjrqwlej?
cbcoutinho

1
раскрой лицо на клавиатуре - нет ничего особенного в 23lkr ... gobbledegook, кроме того, что он не является последовательным или значимым
Сет

9

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


это действительно имеет смысл ввести TeamsPlayers resource.Awesome
Виджей

лучшее объяснение
Диана

1

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

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

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

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


2
Возможно, я просто не понимаю ответ, но этот пост, похоже, не отвечает на вопрос.
BradleyDotNET

Это не дает ответа на вопрос. Чтобы критиковать или запрашивать разъяснения у автора, оставьте комментарий под его постом - вы всегда можете комментировать свои собственные посты, и, когда у вас будет достаточно репутации, вы сможете комментировать любой пост .
Неверный аргумент

4
@IllegalArgument Это является ответом и не будет иметь смысл в качестве комментария. Однако это не самый лучший ответ.
Qix - МОНИКА Плохо обращалась с

1
Этот ответ сложен для понимания и не дает оснований.
Венкат Д.

2
Это не объясняет и не отвечает на заданный вопрос вообще.
Манджит Кумар
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.