web-dev-qa-db-fra.com

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

Imaginez que vous avez 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 gérer les relations. Hibernate s'en sort bien, mais comment exposer cette relation dans une API RESTful?

Je peux penser à plusieurs façons. Tout d'abord, chaque entité peut contenir une liste de l'autre. Ainsi, un objet Player doit avoir une liste des équipes auxquelles il appartient, et chaque objet Team une liste des joueurs qui lui appartiennent. Donc, pour ajouter un joueur à une équipe, il vous suffit de POST sa représentation sur un noeud final, comme par exemple POST /player ou POST /team avec l'objet approprié comme charge utile de la demande. Cela me semble le plus "reposant" mais me semble 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 dont je pourrais envisager de faire cela serait d'exposer la relation en tant que ressource à part entière. Donc, pour voir la liste de tous les joueurs d’une équipe donnée, vous pouvez faire un GET /playerteam/team/{id} ou quelque chose du genre et récupérer une liste d’entités PlayerTeam. Pour ajouter un joueur à une équipe, POST /playerteam avec une entité PlayerTeam correctement construite en tant que 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?

262
Richard Handworker

Dans une interface RESTful, vous pouvez renvoyer des documents décrivant les relations entre les ressources en codant ces relations sous forme de liens. Ainsi, on peut dire qu'une équipe a une ressource de document (/team/{id}/players) qui est une liste de liens vers des joueurs (/player/{id}) de l'équipe et qu'un joueur peut avoir une ressource de document (/player/{id}/teams) c'est une liste de liens vers des équipes dont le joueur est membre. Nice et symétrique. Vous pouvez facilement mapper les opérations de la liste sur cette liste, même en attribuant ses propres identifiants à une relation (on pourrait soutenir qu'ils auraient deux identifiants, selon que vous envisagiez de créer une relation entre l'équipe ou le joueur en premier) si cela facilite les choses. . Le seul problème est que vous devez vous rappeler de supprimer également la relation de l'autre extrémité si vous la supprimez d'une extrémité, mais en la gérant de manière rigoureuse en utilisant un modèle de données sous-jacent, puis en ayant le REST. _ interface soit une vue de ce modèle va rendre cela plus facile.

Les identifiants de relation devraient probablement être basés sur des identificateurs UUID ou quelque chose d'aussi long et aléatoire, quel que soit le type d'identifiant utilisé 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 de relier un joueur et une équipe de manière bidirectionnelle, elles doivent avoir leur propre identité, indépendante des joueurs et des équipes; un groupe GET sur la vue d'équipe du joueur (/player/{playerID}/teams/{teamID}) pourrait alors effectuer une redirection HTTP vers la vue bidirectionnelle (/memberships/{uuid}).

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

113
Donal Fellows

Créez un ensemble séparé de ressources /memberships/.

  1. REST consiste à créer des systèmes évolutifs. À ce moment, vous ne pouvez vous soucier que du fait qu’un joueur soit dans une équipe donnée, mais à un moment dans l’avenir, vous voudrez vouloir annoter celui-ci. relation avec plus de données: combien de temps ont-ils été dans cette équipe, qui les a référés à cette équipe, qui leur entraîneur est/était alors dans cette équipe, etc. etc.
  2. REST dépend de l'efficacité de la mise en cache, ce qui nécessite une attention particulière pour l'atomicité et l'invalidation du cache. Si vous POST une nouvelle entité à /teams/3/players/ cette liste sera invalidée, mais vous ne souhaitez pas que l'URL de remplacement /players/5/teams/ reste en mémoire cache. Oui, différentes caches auront des copies de chaque liste avec des âges différents, et nous ne pouvons rien faire à ce sujet, mais nous pouvons au moins minimiser la confusion pour l'utilisateur POST'ing la mise à jour en limitant le nombre d'entités à invalider. dans le cache local de leur client à un et un seul à /memberships/98745 (voir la discussion de Helland sur les "indices alternatifs" dans La vie au-delà Transactions distribuées pour une discussion plus détaillée).
  3. Vous pouvez implémenter les 2 points ci-dessus en choisissant simplement /players/5/teams ou /teams/3/players (mais pas les deux). Supposons le premier. À un moment donné, cependant, vous voudrez réserver /players/5/teams/ pour une liste des membres actuels , tout en pouvant faire référence à abonnements passés quelque part. Faites de /players/5/memberships/ une liste de liens hypertextes vers /memberships/{id}/ ressources, puis vous pourrez ajouter /players/5/past_memberships/ quand vous le souhaitez, sans avoir à casser les signets de chacun pour les ressources d'appartenance individuelles. C'est un concept général. Je suis sûr que vous pouvez imaginer d'autres avenirs similaires qui s'appliquent davantage à votre cas particulier.
244
fumanchu

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

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

En termes de repos, il est très utile de ne pas penser à SQL et aux jointures, mais plutôt aux collections, aux sous-collections et au parcours.

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 joueurs et équipes.

58
manuel aldana

Les réponses existantes n'expliquent pas les rôles de cohérence et d'idempotency - qui motivent leurs recommandations de UUIDs/nombres aléatoires pour les ID et PUT au lieu de POST.

Si nous considérons le cas où nous avons un scénario simple du type " 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

Cependant, si l'opération client échouait après la POST à /players, nous avons créé un lecteur n'appartenant pas à 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 dans /players/5.

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

Pour éviter d'avoir besoin d'un code de récupération personnalisé, nous pouvons implémenter PUT au lieu de POST.

De la RFC :

l'intention de PUT est 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 à la fois PUT et UUIDs pour Ids.

Cela nous permet de réexécuter à la fois le /playersPUT et le /membershipsPUT sans 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 rien d'autre à faire que de réessayer pour des échecs partiels.

Il s’agit plutôt d’un additif aux réponses existantes, mais j’espère que cela les placera dans le contexte plus général de la flexibilité et de la fiabilité de ReST.

17
Seth

Ma solution préférée consiste à créer trois ressources: Players, Teams et TeamsPlayers.

Ainsi, pour obtenir tous les joueurs d’une équipe, il suffit d’aller dans la ressource Teams et d’obtenir tous ses joueurs en appelant GET /Teams/{teamId}/Players.

D'autre part, pour obtenir toutes les équipes qu'un joueur a jouées, obtenez la ressource Teams dans la Players. Appelez GET /Players/{playerId}/Teams.

Et pour obtenir la relation plusieurs à plusieurs, appelez GET /Players/{playerId}/TeamsPlayers ou GET /Teams/{teamId}/TeamsPlayers.

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

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

Pour traiter la relation n-n, appelez soit GET /Players/{playerId}/TeamsPlayers ou GET /Teams/{teamId}/TeamsPlayers. Ces appels renvoient exactement la ressource, TeamsPlayers.

Cette ressource TeamsPlayers possède les attributs id, playerId, teamId, ainsi que d'autres attributs permettant de décrire la relation. En outre, il dispose des méthodes nécessaires pour y faire face. GET, POST, PUT, DELETE, etc., qui renverront, incluront, mettront à jour, supprimeront la ressource de relation.

La ressource TeamsPlayers implémente certaines requêtes, telles que GET /TeamsPlayers?player={playerId} pour renvoyer toutes les relations TeamsPlayers que le joueur identifié par {playerId} a. Suivant la même idée, utilisez GET /TeamsPlayers?team={teamId} pour renvoyer tous les TeamsPlayers ayant joué dans l’équipe {teamId}. Dans l'un des appels GET, la ressource TeamsPlayers est renvoyée. Toutes les données liées à la relation sont renvoyées.

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

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

  1. Trouver tous les TeamsPlayers que le joueur a id = playerId. (GET /TeamsPlayers?player={playerId})
  2. Boucle le retour TeamsPlayers
  3. En utilisant le teamId obtenu auprès de TeamsPlayers, appelez GET /Teams/{teamId} et stockez les données renvoyées.
  4. Après la boucle se termine. Renvoie toutes les équipes qui ont été mises au courant.

Vous pouvez utiliser le même algorithme pour obtenir tous les joueurs d'une équipe lorsque vous appelez 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 sur REST ressources uniquement. Bien que certains appels supplémentaires puissent être nécessaires pour obtenir des données des joueurs, des équipes ou de leurs relations, 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 Players et Teams sont automatiquement mises à jour.

5
Haroldo Macedo

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

Disons pour PUT

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

Par 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 équipe, nous pourrions procéder comme suit (avec les validations appropriées):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}
1
Heidar Pirzadeh