Як обробити багато-багато-багато відносин у API RESTful?


288

Уявіть, що у вас є 2 особи, Player та Team , де гравці можуть бути в декількох командах. У своїй моделі даних я маю таблицю для кожної сутності та таблицю приєднання, щоб підтримувати відносини. Зимова сплячка чудово справляється з цим, але як я можу викрити ці відносини в API RESTful?

Я можу придумати кілька способів. По-перше, я можу, щоб кожне об'єднання містило список іншого, тому об’єкт Player мав би список команд, до яких він належить, і кожен об'єкт команди мав би список гравців, які належать до нього. Отже, щоб додати гравця до команди, ви просто розмістіть представлення гравця в кінцевій точці, щось на зразок 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 для кожного кінця відносини, не турбуючись про зіткнення (малі цілі числа не мають такої переваги). Якщо ці членські відносини мають будь-які властивості, окрім того, що вони посилаються на гравця та команду у двосторонній спосіб, вони повинні мати власну ідентичність, незалежну як від гравців, так і від команд; a GET на програмі перегляду гравця »( /player/{playerID}/teams/{teamID}) може потім переспрямувати HTTP на двонаправлений вид ( /memberships/{uuid}).

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


265

Складіть окремий набір /memberships/ресурсів.

  1. REST - це створення еволюційних систем, якщо нічого іншого. На даний момент, ви можете тільки дбати про те , що даний гравець знаходиться на тій чи іншій групі, але в якийсь - то момент в майбутньому, ви будете хотіти , щоб коментувати ці відносини з великою кількістю даних: як довго вони були в тій команді, яка передала їх цій команді, хто є їх тренером / перебував у цій команді, тощо.
  2. REST залежить від кешування ефективності, що вимагає певного врахування атомності кеша та недійсності. Якщо ви розмістите нову сутність у /teams/3/players/цьому списку, буде визнано недійсним, але ви не хочете, щоб альтернативна URL-адреса /players/5/teams/залишалася кешованою. Так, різні кеші матимуть копії кожного списку різного віку, і ми з цим не можемо зробити багато, але ми можемо принаймні мінімізувати плутанину для користувача POST'ing оновлення, обмеживши кількість об'єктів, які нам потрібно визнати недійсними в локальному кеші свого клієнта на один і єдиний за адресою /memberships/98745(див. обговорення Гелланда про "альтернативні індекси" в " Життя поза розподіленими транзакціями" для більш детальної дискусії).
  3. Ви можете реалізувати наведені вище пункти, просто вибравши /players/5/teamsабо /teams/3/players(але не обидва). Припустимо перший. У якийсь момент, однак, ви захочете зарезервувати /players/5/teams/список поточних членств, і все ж зможете десь посилатися на минулі членства. Створіть /players/5/memberships/список гіперпосилань на /memberships/{id}/ресурси, а потім ви можете додавати, /players/5/past_memberships/коли вам подобається, не порушуючи всі закладки для окремих ресурсів членства. Це загальне поняття; Я впевнений, що ви можете уявити інші подібні ф'ючерси, які більше стосуються вашого конкретного випадку.

11
Точки 1 та 2 чудово пояснюються, дякую, якщо хтось має більше м’яса для точки 3 у реальному досвіді життя, це допомогло б мені.
Ален

2
Найкраща та найпростіша відповідь ІМО спасибі! Маючи дві кінцеві точки та синхронізувати їх, існує безліч ускладнень.
Венкат Д.

7
привіт фуманчу. Запитання: У кінцевій точці решти / членства / 98745, що означає це число в кінці URL-адреси? Це унікальний ідентифікатор для членства? Як би взаємодіяти з кінцевою точкою членства? Щоб додати гравця, чи буде відправлено POST, що містить корисну навантаження з {team: 3, player: 6}, тим самим створивши зв'язок між ними? Що з GET? чи надішлете ви GET в / членства? player = та / membersihps? team =, щоб отримати результати? Це ідея? Я щось пропускаю? (Я намагаюся дізнатися спокійні кінцеві точки) Чи є в цьому випадку ідентифікатор 98745 у членстві / 98745 справді корисним?
aruuuuu

@aruuuuu окрему кінцеву точку для асоціації слід забезпечити сурогатним ПК. Це значно полегшує життя також в цілому: / memberships / {članstvoId}. Ключ (playerId, teamId) залишається унікальним і тому може бути використаний на ресурсах, що мають таке відношення: / команд / {teamId} / гравців та / гравців / {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, який краще обробляє ваші n: n відносини гравців та команд.


20
Що робити, якщо team_player має додаткову інформацію, наприклад статус тощо? де ми представляємо це у вашій моделі? чи можемо ми рекламувати його на ресурсі та надавати URL-адреси для нього, як гра /, гравець /
Narendra Kamma

Привіт, швидке запитання, щоб переконатися, що я правильно розумію: GET / команд / 1 / гравців / 3 повертає порожнє тіло відповіді. Єдина змістовна відповідь на це - 200 проти 404. Інформація про суть гравця (ім'я, вік тощо) НЕ повертається GET / командами / 1 / гравцями / 3. Якщо клієнт хоче отримати додаткову інформацію про програвача, він повинен GET / player / 3. Це все правильно?
Вердагон

2
Я згоден з Вашим картографуванням, але у мене є одне питання. Це питання особистої думки, але що ви думаєте про POST / команд / 1 / гравців і чому ви не використовуєте це? Чи бачите ви якийсь недолік / оманливий у такому підході?
JakubKnejzlik

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

Однак, якщо операція покупець не в змозі після того , як 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і 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

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

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


У цій гіпотетичній кінцевій точці звідки ви взялися 23lkrjrqwlej?
cbcoutinho

1
обличчя клавіатури на клавіатурі - немає нічого особливого в 23lkr ... gobbledegook, крім того, що це не є послідовним чи значущим
Seth

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. Знайти всі TeamsPlayers , що гравець має ідентифікатор = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Цикл повернутих гравців Teams
  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 ресурс. Чудовий
vijay

найкраще пояснення
Діана

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. / гравці (це основний ресурс)
  2. / команд / {id} / гравців (це ресурс відносин, тому він реагує різним, що 1)
  3. / членство (відносини, але семантично складні)
  4. / гравці / членство (відносини, але семантично складні)

Я віддаю перевагу 2


2
Можливо, я просто не розумію відповіді, але ця публікація, схоже, не відповідає на питання.
BradleyDotNET

Це не дає відповіді на запитання. Щоб критикувати або вимагати роз'яснення у автора, залиште коментар під їх публікацією - ви завжди можете коментувати свої власні публікації, і як тільки у вас буде достатня репутація, ви зможете коментувати будь-яку публікацію .
Незаконний аргумент

4
@IllegalArgument Це є відповіддю і не матиме сенс в якості коментаря. Однак це не найбільша відповідь.
Qix - МОНІКА ПОМИЛИЛА

1
Цю відповідь важко дотримуватися і не дає причин.
Венкат Д.

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