Comment gérer les relations plusieurs-à-plusieurs dans une API RESTful?

288

Imaginez que vous ayez 2 entités, Player et Team , où les joueurs peuvent faire partie de plusieurs équipes. Dans mon modèle de données, j'ai une table pour chaque entité et une table de jointure pour maintenir les relations. Hibernate gère bien cela, mais comment pourrais-je exposer cette relation dans une API RESTful?

Je peux penser à deux façons. Tout d'abord, je pourrais faire en sorte que chaque entité contienne une liste de l'autre, donc un objet Player aurait une liste des équipes auxquelles il appartient, et chaque objet Team aurait une liste des joueurs qui lui appartiennent. Donc, pour ajouter un joueur à une équipe, il vous suffirait de POSTER la représentation du joueur sur un point de terminaison, quelque chose comme POST /playerou POST /teamavec l'objet approprié comme charge utile de la demande. Cela me semble le plus "reposant" mais ça fait un peu bizarre.

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

L'autre façon de penser à cela serait d'exposer la relation comme une ressource à part entière. Donc, pour voir une liste de tous les joueurs d'une équipe donnée, vous pouvez faire un GET /playerteam/team/{id}ou quelque chose comme ça et récupérer une liste d'entités PlayerTeam. Pour ajouter un joueur à une équipe, POST /playerteamavec une entité PlayerTeam correctement construite comme charge utile.

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

Quelle est la meilleure pratique pour cela?

Richard Handworker
la source

Réponses:

129

Dans une interface RESTful, vous pouvez renvoyer des documents qui décrivent les relations entre les ressources en codant ces relations sous forme de liens. Ainsi, une équipe peut être considérée comme ayant une ressource document ( /team/{id}/players) qui est une liste de liens vers les joueurs ( /player/{id}) de l'équipe, et un joueur peut avoir une ressource document (/player/{id}/teams) qui est une liste de liens vers des équipes dont le joueur est membre. Agréable et symétrique. Vous pouvez les opérations de carte sur cette liste assez facilement, même en donnant à une relation ses propres identifiants (sans doute, ils auraient deux identifiants, selon que vous envisagez la relation en premier ou en premier) si cela facilite les choses. . Le seul point délicat est que vous devez vous rappeler de supprimer la relation de l'autre extrémité également si vous la supprimez d'une extrémité, mais de la gérer rigoureusement en utilisant un modèle de données sous-jacent, puis en ayant l'interface REST comme une vue de ce modèle va rendre cela plus facile.

Les identifiants de relation doivent probablement être basés sur des UUID ou quelque chose d'aussi long et aléatoire, quel que soit le type d'ID que vous utilisez pour les équipes et les joueurs. Cela vous permettra d'utiliser le même UUID que le composant ID pour chaque extrémité de la relation sans vous soucier des collisions (les petits entiers n'ont pas cet avantage). Si ces relations d'appartenance ont des propriétés autres que le simple fait qu'elles mettent en relation un joueur et une équipe de manière bidirectionnelle, elles doivent avoir leur propre identité indépendante des joueurs et des équipes; un GET sur la vue du joueur »de l'équipe ( /player/{playerID}/teams/{teamID}) pourrait alors faire une redirection HTTP vers la vue bidirectionnelle ( /memberships/{uuid}).

Je recommande d'écrire des liens dans tous les documents XML que vous retournez (si vous produisez du XML bien sûr) en utilisant des attributs XLink xlink:href .

Associés Donal
la source
265

Créez un ensemble de /memberships/ressources distinct .

  1. REST consiste à créer des systèmes évolutifs si rien d'autre. En ce moment, vous ne pouvez veiller à ce que un joueur donné est une équipe donnée, mais à un moment donné à l'avenir, vous allez vouloir annoter cette relation avec d' autres données: combien de temps ils ont été sur cette équipe, qui les a renvoyés à cette équipe, qui est / était son entraîneur dans cette équipe, etc.
  2. REST dépend de la mise en cache pour l'efficacité, ce qui nécessite une certaine considération pour l'atomicité et l'invalidation du cache. Si vous POSTEZ une nouvelle entité à /teams/3/players/cette liste sera invalidée, mais vous ne voulez pas que l'URL alternative /players/5/teams/reste en cache. Oui, différents caches auront des copies de chaque liste avec des âges différents, et nous ne pouvons pas faire grand-chose à ce sujet, mais nous pouvons au moins minimiser la confusion pour l'utilisateur POSTANT la mise à jour en limitant le nombre d'entités dont nous avons besoin pour invalider dans le cache local de leur client à un et un seul à /memberships/98745(voir la discussion de Helland sur les "indices alternatifs" dans Life Beyond Distributed Transactions pour une discussion plus détaillée).
  3. Vous pouvez implémenter les 2 points ci-dessus en choisissant simplement /players/5/teamsou /teams/3/players(mais pas les deux). Supposons le premier. À un certain moment, cependant, vous voudrez réserver /players/5/teams/une liste des adhésions actuelles , et cependant être en mesure de vous référer quelque part aux anciennes adhésions. Faites /players/5/memberships/une liste d'hyperliens vers des /memberships/{id}/ressources, puis vous pouvez ajouter /players/5/past_memberships/quand vous le souhaitez, sans avoir à casser les signets de chacun pour les ressources d'adhésion individuelles. Il s'agit d'un concept général; Je suis sûr que vous pouvez imaginer d'autres futurs similaires qui sont plus applicables à votre cas spécifique.
fumanchu
la source
11
Les points 1 et 2 sont parfaitement expliqués, merci, si quelqu'un a plus de viande pour le point 3 dans l'expérience de la vie réelle, cela m'aiderait.
Alain
2
Meilleure et plus simple réponse merci OMI! Le fait d'avoir deux points d'extrémité et de les maintenir synchronisés a une multitude de complications.
Venkat D.
7
salut fumanchu. Questions: Dans les autres points de terminaison / adhésions / 98745, que représente ce nombre à la fin de l'URL? Est-ce un identifiant unique pour les membres? Comment interagirait-on avec le point de terminaison des adhésions? Pour ajouter un joueur, un POST contenant une charge utile avec {team: 3, player: 6} serait-il envoyé, créant ainsi le lien entre les deux? Et un GET? enverriez-vous un GET à / memberships? player = et / membersihps? team = pour obtenir des résultats? C'est ça l'idée? Suis-je en train de manquer quelque chose? (J'essaie d'apprendre des points de terminaison reposants) Dans ce cas, l'ID 98745 dans les adhésions / 98745 est-il vraiment utile?
aruuuuu
@aruuuuu un point de terminaison distinct pour une association doit être fourni avec un PK de substitution. Cela rend la vie beaucoup plus facile aussi en général: / memberships / {membershipId}. La clé (playerId, teamId) reste unique et peut donc être utilisée sur les ressources qui possèdent cette relation: / équipes / {teamId} / joueurs et / joueurs / {playerId} / équipes. Mais ce n'est pas toujours lorsque de telles relations sont maintenues des deux côtés. Par exemple, Recettes et ingrédients: vous n'aurez presque jamais besoin d'utiliser / Ingrédients / {ingrédientId} / Recettes /.
Alexander Palamarchuk
65

Je mapperais une telle relation avec les sous-ressources, la conception / traversée générale serait alors:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

En termes reposants, cela aide beaucoup à ne pas penser à SQL et aux jointures, mais plutôt aux collections, sous-collections et traversées.

Quelques exemples:

# 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

Comme vous le voyez, je n'utilise pas POST pour placer des joueurs dans des équipes mais PUT, qui gère mieux votre relation n: n entre les joueurs et les équipes.

manuel aldana
la source
20
Que se passe-t-il si team_player a des informations supplémentaires comme le statut, etc.? où le représentons-nous dans votre modèle? pouvons-nous le promouvoir en tant que ressource et fournir des URL pour celui-ci, tout comme le jeu /, le joueur /
Narendra Kamma
Hé, question rapide juste pour m'assurer que je comprends bien: GET / équipes / 1 / joueurs / 3 renvoie un corps de réponse vide. La seule réponse significative de cette situation est de 200 contre 404. Les informations sur l'entité du joueur (nom, âge, etc.) ne sont PAS retournées par GET / équipes / 1 / joueurs / 3. Si le client souhaite obtenir des informations supplémentaires sur le joueur, il doit OBTENIR / joueurs / 3. Est-ce que tout est correct?
Verdagon
2
Je suis d'accord avec votre cartographie, mais j'ai une question. C'est une question d'opinion personnelle, mais que pensez-vous du POST / équipes / 1 / joueurs et pourquoi ne l'utilisez-vous pas? Voyez-vous un inconvénient / une erreur dans cette approche?
JakubKnejzlik
2
POST n'est pas idempotent, c'est-à-dire que si vous effectuez POST / équipes / 1 / joueurs n fois, vous changeriez n fois / équipes / 1. mais déplacer un joueur vers / équipes / 1 n fois ne changera pas l'état de l'équipe, donc l'utilisation de PUT est plus évidente.
manuel aldana
1
@NarendraKamma Je suppose qu'il suffit d'envoyer statuscomme paramètre dans la demande PUT? Y a-t-il un inconvénient à cette approche?
Traxo
22

Les réponses existantes n'expliquent le rôle de la cohérence et idempotence - qui motivent leurs recommandations UUIDs/ nombres aléatoires pour les ID et au PUTlieu de POST.

Si nous considérons le cas où nous avons un scénario simple comme " Ajouter un nouveau joueur à une équipe ", nous rencontrons des problèmes de cohérence.

Parce que le joueur n'existe pas, nous devons:

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

Toutefois, si l' échec de l'opération client après POSTpour /players, nous avons créé un joueur qui ne appartiennent à une équipe:

POST /players { "Name": "Murray" } //=> 302 /players/5
// *client failure*
// *client retries naively*
POST /players { "Name": "Murray" } //=> 302 /players/6
POST /teams/1/players/6

Nous avons maintenant un lecteur dupliqué orphelin /players/5.

Pour résoudre ce problème, nous pouvons écrire un code de récupération personnalisé qui recherche les joueurs orphelins qui correspondent à une clé naturelle (par exemple Name). Il s'agit d'un code personnalisé qui doit être testé, coûte plus d'argent et de temps, etc., etc.

Pour éviter d'avoir besoin d'un code de récupération personnalisé, nous pouvons l'implémenter à la PUTplace de POST.

Du RFC :

l'intention PUTest idempotente

Pour qu'une opération soit idempotente, elle doit exclure les données externes telles que les séquences d'ID générées par le serveur. C'est pourquoi les gens recommandent les deux PUTet les UUIDs pour les Ids ensemble.

Cela nous permet de relancer à la fois le /players PUTet le /memberships PUTsans conséquences:

PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
// *client failure*
// *client YOLOs*
PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
PUT /teams/1/players/23lkrjrqwlej

Tout va bien et nous n'avons pas eu besoin de faire autre chose que de réessayer pour des échecs partiels.

Il s'agit davantage d'un addendum aux réponses existantes, mais j'espère que cela les replace dans le contexte d'une vue d'ensemble de la flexibilité et de la fiabilité de ReST.

Seth
la source
Dans ce point final hypothétique, d'où avez-vous obtenu le 23lkrjrqwlej?
cbcoutinho
1
rouler la face sur le clavier - le 23lkr n'a rien de spécial ... gobbledegook à part qu'il n'est pas séquentiel ou significatif
Seth
9

Ma solution préférée est de créer trois ressources: Players, Teamset TeamsPlayers.

Donc, pour obtenir tous les joueurs d'une équipe, allez simplement à la Teamsressource et obtenez tous ses joueurs en appelant GET /Teams/{teamId}/Players.

D'un autre côté, pour obtenir toutes les équipes qu'un joueur a jouées, obtenez la Teamsressource dans le Players. Appelle GET /Players/{playerId}/Teams.

Et, pour obtenir l'appel de relation plusieurs-à-plusieurs GET /Players/{playerId}/TeamsPlayersou GET /Teams/{teamId}/TeamsPlayers.

Notez que, dans cette solution, lorsque vous appelez GET /Players/{playerId}/Teams, vous obtenez un tableau de Teamsressources, c'est exactement la même ressource que vous obtenez lorsque vous appelez GET /Teams/{teamId}. L'inverse suit le même principe, vous obtenez un tableau de Playersressources lors de l'appel GET /Teams/{teamId}/Players.

Dans les deux appels, aucune information sur la relation n'est renvoyée. Par exemple, aucun contractStartDaten'est renvoyé, car la ressource renvoyée ne contient aucune information sur la relation, uniquement sur sa propre ressource.

Pour gérer la relation nn, appelez soit GET /Players/{playerId}/TeamsPlayersou GET /Teams/{teamId}/TeamsPlayers. Ces appels renvoient la ressource exactement TeamsPlayers.

Cette TeamsPlayersressource a id, playerId, teamIdattributs, ainsi que d'autres pour décrire la relation. En outre, il dispose des méthodes nécessaires pour y faire face. GET, POST, PUT, DELETE etc. qui renverra, inclura, mettra à jour, supprimera la ressource de relation.

La TeamsPlayersressource implémente certaines requêtes, comme GET /TeamsPlayers?player={playerId}retourner toutes les TeamsPlayersrelations identifiées par le joueur{playerId} . Suivant la même idée, utilisez GET /TeamsPlayers?team={teamId}pour retourner tous ceux TeamsPlayersqui ont joué dans l' {teamId}équipe. Dans les deux GETappels, la ressource TeamsPlayersest renvoyée. Toutes les données liées à la relation sont retournées.

Lors de l'appel GET /Players/{playerId}/Teams(ou GET /Teams/{teamId}/Players), la ressource Players(ou Teams) appelle TeamsPlayerspour renvoyer les équipes (ou joueurs) associés à l'aide d'un filtre de requête.

GET /Players/{playerId}/Teams fonctionne comme ceci:

  1. Trouvez tous les TeamsPlayers que le joueur a id = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Boucler les TeamsPlayers retournés
  3. À l'aide du teamId obtenu auprès de TeamsPlayers , appelez GET /Teams/{teamId}et stockez les données renvoyées
  4. Une fois la boucle terminée. Renvoyez toutes les équipes qui se sont mises dans la boucle.

Vous pouvez utiliser le même algorithme pour obtenir tous les joueurs d'une équipe, lors de l'appel GET /Teams/{teamId}/Players, mais en échangeant des équipes et des joueurs.

Mes ressources ressembleraient à ceci:

/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',
}

Cette solution repose uniquement sur les ressources REST. Bien que certains appels supplémentaires puissent être nécessaires pour obtenir des données des joueurs, des équipes ou de leur relation, toutes les méthodes HTTP sont facilement implémentées. POST, PUT, DELETE sont simples et directs.

Chaque fois qu'une relation est créée, mise à jour ou supprimée, les ressources Playerset les Teamsressources sont automatiquement mises à jour.

Haroldo Macedo
la source
il est vraiment logique de présenter la ressource
TeamsPlayers.Awesome
meilleure explication
Diana
1

Je sais qu'il y a une réponse marquée comme acceptée pour cette question, cependant, voici comment nous pourrions résoudre les problèmes soulevés précédemment:

Disons pour PUT

PUT    /membership/{collection}/{instance}/{collection}/{instance}/

À titre d'exemple, les éléments suivants auront tous le même effet sans nécessiter de synchronisation, car ils sont effectués sur une seule ressource:

PUT    /membership/teams/team1/players/player1/
PUT    /membership/players/player1/teams/team1/

maintenant, si nous voulons mettre à jour plusieurs adhésions pour une même équipe, nous pouvons procéder comme suit (avec les validations appropriées):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}
Heidar Pirzadeh
la source
-3
  1. / players (est une ressource principale)
  2. / équipes / {id} / joueurs (est une ressource relationnelle, donc elle réagit différemment de 1)
  3. / appartenances (est une relation mais sémantiquement compliquée)
  4. / joueurs / appartenances (est une relation mais sémantiquement compliquée)

Je préfère 2

MoaLaiSkirulais
la source
2
Peut-être que je ne comprends tout simplement pas la réponse, mais ce post ne semble pas répondre à la question.
BradleyDotNET
Cela ne fournit pas de réponse à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous son article - vous pouvez toujours commenter vos propres articles, et une fois que vous en avez suffisamment réputation vous pourrez commenter n'importe quel article .
Argument illégal
4
@IllegalArgument It est une réponse et ne serait pas logique comme un commentaire. Cependant, ce n'est pas la meilleure réponse.
Qix - MONICA A ÉTÉ BRUÉE le
1
Cette réponse est difficile à suivre et ne fournit aucune raison.
Venkat D.
2
Cela n'explique pas ou ne répond pas du tout à la question posée.
Manjit Kumar