How to handle many-to-many relationships in a RESTful API?
Clash Royale CLAN TAG#URR8PPP
How to handle many-to-many relationships in a RESTful API?
Imagine you have 2 entities, Player and Team, where players can be on multiple teams. In my data model, I have a table for each entity, and a join table to maintain the relationships. Hibernate is fine at handling this, but how might I expose this relationship in a RESTful API?
I can think of a couple ways. First, I might have each entity contain a list of the other, so a Player object would have a list of Teams it belongs to, and each Team object would have a list of Players that belong to it. So to add a Player to a Team, you would just POST the player's representation to an endpoint, something like POST /player
or POST /team
with the appropriate object as the payload of the request. This seems the most "RESTful" to me but feels a little weird.
/player
/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'
The other way I can think of to do this would be to expose the relationship as a resource in its own right. So to see a list of all the players on a given team, you might do a GET /playerteam/team/id
or something like that and get back a list of PlayerTeam entities. To add a player to a team, POST /playerteam
with an appropriately built PlayerTeam entity as the payload.
/playerteam/team/id
/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'
]
What is the best practice for this?
7 Answers
7
In a RESTful interface, you can return documents that describe the relationships between resources by encoding those relationships as links. Thus, a team can be said to have a document resource (/team/id/players
) that is a list of links to players (/player/id
) on the team, and a player can have a document resource (/player/id/teams
) that is a list of links to teams that the player is a member of. Nice and symmetric. You can the map operations on that list easily enough, even giving a relationship its own IDs (arguably they'd have two IDs, depending on whether you're thinking about the relationship team-first or player-first) if that makes things easier. The only tricky bit is that you've got to remember to delete the relationship from the other end as well if you delete it from one end, but rigorously handling this by using an underlying data model and then having the REST interface be a view of that model is going to make that easier.
/team/id/players
/player/id
/player/id/teams
Relationship IDs probably ought to be based on UUIDs or something equally long and random, irrespective of whatever type of IDs you use for teams and players. That will let you use the same UUID as the ID component for each end of the relationship without worrying about collisions (small integers do not have that advantage). If these membership relationships have any properties other than the bare fact that they relate a player and a team in a bidirectional fashion, they should have their own identity that is independent of both players and teams; a GET on the player»team view (/player/playerID/teams/teamID
) could then do an HTTP redirect to the bidirectional view (/memberships/uuid
).
/player/playerID/teams/teamID
/memberships/uuid
I recommend writing links in any XML documents you return (if you happen to be producing XML of course) using XLink xlink:href
attributes.
xlink:href
Make a separate set of /memberships/
resources.
/memberships/
/teams/3/players/
/players/5/teams/
/memberships/98745
/players/5/teams
/teams/3/players
/players/5/teams/
/players/5/memberships/
/memberships/id/
/players/5/past_memberships/
Point 1 & 2 are perfectly explained, thanks, if anybody has more meat for the point 3 in real life experience, that would help me.
– Alain
Nov 25 '13 at 21:27
Best and simplest answer IMO thanks! Having two endpoints and keeping them in sync has a host of complications.
– Venkat D.
Jan 8 '16 at 1:57
hi fumanchu. Questions: In the rest endpoint /memberships/98745 what does that number at the end of the url represent? Is it a unique id for the membership? How would one interact with the memberships endpoint? To add a player would a POST be sent containing a payload with team : 3, player : 6 , thereby creating the link between the two? What about a GET ? would you send a GET to /memberships?player= and /membersihps?team= to get results ? That is the idea? Am I missing anything? (I am trying to learn restful endpoints) In that case, is the id 98745 in memberships/98745 ever really useful?
– aruuuuu
Jan 6 '17 at 2:45
Please update the link, it is currently not working.
– shankbond
Mar 30 '17 at 5:40
I would map such relationship with sub-resources, general design/traversal would then be:
# team resource
/teams/teamId
# players resource
/players/playerId
# teams/players subresource
/teams/teamId/players/playerId
In Restful-terms it helps a lot in not thinking of SQL and joins but more into collections, sub-collections and traversal.
Some examples:
# 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
As you see I don't use POST for placing players to teams but PUT, which handles your n:n relationship of players and teams better.
What if team_player is having additional information like status etc? where do we represent it in your model? can we promote it to a resource, and provide URLs for it, just like game/, player/
– Narendra Kamma
Feb 4 '13 at 5:33
Hey, quick question just to make sure i'm getting this right: GET /teams/1/players/3 returns an empty response body. The only meaningful response from this is 200 vs 404. The player entity's information (name, age, etc) is NOT returned by GET /teams/1/players/3. If the client wants to get additional information on the player, he must GET /players/3. Is this all correct?
– Verdagon
May 15 '13 at 22:12
I agree with Your mapping, but have one question. It's matter of personal opinion, but what do you think about POST /teams/1/players and why don't You use it? Do you see any disadvantage/misleading in this approach?
– JakubKnejzlik
Dec 19 '14 at 5:00
POST is not idempotent, i.e. if you do POST /teams/1/players n-times, you would change n-times /teams/1. but moving a player to /teams/1 n-times won't change the team's state so using PUT is more obvious.
– manuel aldana
Dec 19 '14 at 14:32
@NarendraKamma I presume just send
status
as param in PUT request? Is there a downside to that approach?– Traxo
Jan 4 at 9:59
status
The existing answers don't explain the roles of consistency and idempotency - which motivate their recommendations of UUIDs
/random numbers for IDs and PUT
instead of POST
.
UUIDs
PUT
POST
If we consider the case where we have a simple scenario like "Add a new player to a team", we encounter consistency issues.
Because the player doesn't exist, we need to:
POST /players "Name": "Murray" //=> 302 /players/5
POST /teams/1/players/5
However, should the client operation fail after the POST
to /players
, we've created a player that doesn't belong to a team:
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
Now we have an orphaned duplicate player in /players/5
.
/players/5
To fix this we might write custom recovery code that checks for orphaned players that match some natural key (e.g. Name
). This is custom code that needs to be tested, costs more money and time etc etc
Name
To avoid needing custom recovery code, we can implement PUT
instead of POST
.
PUT
POST
From the RFC:
the intent of PUT
is idempotent
PUT
For an operation to be idempotent, it needs to exclude external data such as server-generated id sequences. This is why people are recommending both PUT
and UUID
s for Id
s together.
PUT
UUID
Id
This allows us to rerun both the /players
PUT
and the /memberships
PUT
without consequences:
/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
Everything is fine and we didn't need to do anything more than retry for partial failures.
This is more of an addendum to the existing answers but I hope it puts them in context of the bigger picture of just how flexible and reliable ReST can be.
My preferred solution is to create three resources: Players
, Teams
and TeamsPlayers
.
Players
Teams
TeamsPlayers
So, to get all the players of a team, just go to Teams
resource and get all its players by calling GET /Teams/teamId/Players
.
Teams
GET /Teams/teamId/Players
On the other hand, to get all the teams a player has played, get the Teams
resource within the Players
. Call GET /Players/playerId/Teams
.
Teams
Players
GET /Players/playerId/Teams
And, to get the many-to-many relationship call GET /Players/playerId/TeamsPlayers
or GET /Teams/teamId/TeamsPlayers
.
GET /Players/playerId/TeamsPlayers
GET /Teams/teamId/TeamsPlayers
Note that, in this solution, when you call GET /Players/playerId/Teams
, you get an array of Teams
resources, that is exactly the same resource you get when you call GET /Teams/teamId
. The reverse follows the same principle, you get an array of Players
resources when call GET /Teams/teamId/Players
.
GET /Players/playerId/Teams
Teams
GET /Teams/teamId
Players
GET /Teams/teamId/Players
In either calls, no information about the relationship is returned. For example, no contractStartDate
is returned, because the resource returned has no info about the relationship, only about its own resource.
contractStartDate
To deal with the n-n relationship, call either GET /Players/playerId/TeamsPlayers
or GET /Teams/teamId/TeamsPlayers
. These calls return the exactly resource, TeamsPlayers
.
GET /Players/playerId/TeamsPlayers
GET /Teams/teamId/TeamsPlayers
TeamsPlayers
This TeamsPlayers
resource has id
, playerId
, teamId
attributes, as well as some others to describe the relationship. Also, it has the methods necessary to deal with them. GET, POST, PUT, DELETE etc that will return, include, update, remove the relationship resource.
TeamsPlayers
id
playerId
teamId
The TeamsPlayers
resource implements some queries, like GET /TeamsPlayers?player=playerId
to return all TeamsPlayers
relationships the player identified by playerId
has. Following the same idea, use GET /TeamsPlayers?team=teamId
to return all the TeamsPlayers
that have played in the teamId
team.
In either GET
call, the resource TeamsPlayers
is returned. All the data related to the relationship is returned.
TeamsPlayers
GET /TeamsPlayers?player=playerId
TeamsPlayers
playerId
GET /TeamsPlayers?team=teamId
TeamsPlayers
teamId
GET
TeamsPlayers
When calling GET /Players/playerId/Teams
(or GET /Teams/teamId/Players
), the resource Players
(or Teams
) calls TeamsPlayers
to return the related teams (or players) using a query filter.
GET /Players/playerId/Teams
GET /Teams/teamId/Players
Players
Teams
TeamsPlayers
GET /Players/playerId/Teams
works like this:
GET /Players/playerId/Teams
GET /TeamsPlayers?player=playerId
GET /Teams/teamId
You can use the same algorithm to get all players from a team, when calling GET /Teams/teamId/Players
, but exchanging teams and players.
GET /Teams/teamId/Players
My resources would look like this:
/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',
This solution relies on REST resources only. Although some extra calls may be necessary to get data from players, teams or their relationship, all HTTP methods are easily implemented. POST, PUT, DELETE are simple and straightforward.
Whenever a relationship is created, updated or deleted, both Players
and Teams
resources are automatically updated.
Players
Teams
it really makes sense to introduce TeamsPlayers resource.Awesome
– vijay
Sep 9 at 11:22
I know that there's an answer marked as accepted for this question, however, here is how we could solve the previously raised issues:
Let's say for PUT
PUT /membership/collection/instance/collection/instance/
As an example, the followings will all result in the same effect without a need for syncing because they are done on a single resource:
PUT /membership/teams/team1/players/player1/
PUT /membership/players/player1/teams/team1/
now if we want to update multiple memberships for one team we could do as follows (with proper validations):
PUT /membership/teams/team1/
membership: [
teamId: "team1"
playerId: "player1"
,
teamId: "team1"
playerId: "player2"
,
...
]
I prefer 2
Perhaps I just don't understand the answer, but this post does not seem to answer the question.
– BradleyDotNET
Jun 21 '14 at 5:39
This does not provide an answer to the question. To critique or request clarification from an author, leave a comment below their post - you can always comment on your own posts, and once you have sufficient reputation you will be able to comment on any post.
– Illegal Argument
Jun 21 '14 at 5:42
@IllegalArgument It is an answer and wouldn't make sense as a comment. However, it's not the greatest answer.
– Qix
Jun 21 '14 at 6:08
This answer is difficult to follow and doesn't provide reasons.
– Venkat D.
Jan 8 '16 at 1:59
This doesn't explain or answers the asked question at all.
– Manjit Kumar
Feb 1 '16 at 12:09
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Brilliant, thanks for this answer!
– Tomasz Zielinski
Sep 15 '13 at 16:21