How can I handle many-to-many relationships in a RESTful API?
Asked Answered
B

7

351

Imagine you have two 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 of 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.

/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.

/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?

Begorra answered 12/6, 2011 at 20:43 Comment(0)
L
149

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.

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}).

I recommend writing links in any XML documents you return (if you happen to be producing XML of course) using XLink xlink:href attributes.

Laicize answered 13/6, 2011 at 8:30 Comment(0)
K
302

Make a separate set of /memberships/ resources.

  1. REST is about making evolvable systems if nothing else. At this moment, you may only care that a given player is on a given team, but at some point in the future, you will want to annotate that relationship with more data: how long they've been on that team, who referred them to that team, who their coach is/was while on that team, etc etc.
  2. REST depends on caching for efficiency, which requires some consideration for cache atomicity and invalidation. If you POST a new entity to /teams/3/players/ that list will be invalidated, but you don't want the alternate URL /players/5/teams/ to remain cached. Yes, different caches will have copies of each list with different ages, and there's not much we can do about that, but we can at least minimize the confusion for the user POST'ing the update by limiting the number of entities we need to invalidate in their client's local cache to one and only one at /memberships/98745 (see Helland's discussion of "alternate indices" in Life beyond Distributed Transactions for a more detailed discussion).
  3. You could implement the above 2 points by simply choosing /players/5/teams or /teams/3/players (but not both). Let's assume the former. At some point, however, you will want to reserve /players/5/teams/ for a list of current memberships, and yet be able to refer to past memberships somewhere. Make /players/5/memberships/ a list of hyperlinks to /memberships/{id}/ resources, and then you can add /players/5/past_memberships/ when you like, without having to break everyone's bookmarks for the individual membership resources. This is a general concept; I'm sure you can imagine other similar futures which are more applicable to your specific case.
Kameko answered 13/6, 2011 at 16:16 Comment(4)
Point 1 & 2 are perfectly explained, thanks, if anybody has more meat for the point 3 in real life experience, that would help me.Asturias
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?Fetid
@Fetid a separate endpoint for an association should be provided with a surrogate PK. It makes the life a lot easier also in general: /memberships/{membershipId}. The key (playerId, teamId) remains unique and thus can be used on the resources that posses this relation: /teams/{teamId}/players and /players/{playerId}/teams. But it's not always when such relations are maintained on both sides. For example, Recipes and Ingredients: you will hardly ever need to use /ingredients/{ingredientId}/recipes/.Idealism
If you consider here "Membership" as equivalent to "TeamPlayers" as explained https://mcmap.net/q/92904/-how-can-i-handle-many-to-many-relationships-in-a-restful-api here above. the two answers are similar and complimentary and both great. However, for complex API's i would advise not to use too many names for the same idea (keep membership available for user memberships for instance). api/teamplayers/<id> is perfectly fine.Marrymars
L
149

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.

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}).

I recommend writing links in any XML documents you return (if you happen to be producing XML of course) using XLink xlink:href attributes.

Laicize answered 13/6, 2011 at 8:30 Comment(0)
S
77

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.

Sidestroke answered 13/6, 2011 at 8:11 Comment(10)
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/Brail
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?Sleigh
Manual, Any reason you don't use /players and /player where you need to get all or get one result(s)?Planetary
@Verdagon: Yes, if /teams/1/players/3 returns 404 (players doesn't belong to team or has quit), it might return 200 for /players/3.Sidestroke
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?Dzerzhinsk
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.Sidestroke
@NarendraKamma I presume just send status as param in PUT request? Is there a downside to that approach?Drus
POST /teams/:teamid/players might be considered to support adding multiple players at once, placing the player ids in an array in the body. However, it would not be symmetric since DELETE could not be used to remove multiple players from a team. I would rather use the PUT design in this answer and possibly complement with PUT /teams/:teamid/players in which the entire list of players in a team could be replaced, hence supporting multiple additions and removals (a GET followed by a PUT). For most situations the endpoints in this answer are probably enough though.Dapplegray
what if my client wants to provide an table for new players for this team. so the datatable should only contain players which are not part of a team. how could this endpoint be reachable? i am searching for something like /team/1/notplayersLucio
I agree POST is not idempotent, but PUT is not the correct verb to create new resource anyway. For adding player 3 also to team 2, I would do POST to /teams/2/players instead and put the player Id 3 in the body, because you're indeed trying to add player 3 to the player list in team 2. Or to be exact, you're trying to create the player team relationship. The 2nd time you call the endpoint, you should throw an exception saying "player 3 is already in team 2".Muller
P
35

My preferred solution is to create three resources: Players, Teams and 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.

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.

And, to get the many-to-many relationship call GET /Players/{playerId}/TeamsPlayers or 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.

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.

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.

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.

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.

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 works like this:

  1. Find all TeamsPlayers that the player has id = playerId. (GET /TeamsPlayers?player={playerId})
  2. Loop the returned TeamsPlayers
  3. Using the teamId obtained from TeamsPlayers, call GET /Teams/{teamId} and store the returned data
  4. After the loop finishes. Return all teams that were got in the loop.

You can use the same algorithm to get all players from a team, when calling GET /Teams/{teamId}/Players, but exchanging teams and 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.

Pulmonic answered 8/8, 2018 at 13:40 Comment(0)
B
26

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.

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" } //=> 201 /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 { "Name": "Murray" } //=> 201 /players/5
// *client failure*
// *client retries naively*
POST /players { "Name": "Murray" } //=> 201 /players/6
POST /teams/1/players/6

Now we have an orphaned duplicate player in /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

To avoid needing custom recovery code, we can implement PUT instead of POST.

From the RFC:

the intent of PUT is idempotent

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 UUIDs for Ids together.

This allows us to rerun both the /players PUT and the /memberships PUT without consequences:

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.

Barleycorn answered 6/1, 2017 at 3:26 Comment(6)
In this hypothetical endpoint, where did you get the 23lkrjrqwlej from?Godrich
roll face on keyboard - there's nothing special about the 23lkr... gobbledegook other than that it isn't sequential or meaningfulBarleycorn
What if the client exits before retry? If a player cant exist without a team shouldn't this be a transaction on the server?Mallet
@Mallet I think you're thinking of locking distributed transactions. If you're doing it without locks then any failure and delivery tolerant orchestration of the transaction essentially boils down to validation and retries with clients taking responsibility for reliably persisting incomplete system states. One way that people have done this is to transact the messages into some reliable, persistent queue or event stream; this is really simple if the receivers behave in the way I've mentioned above. That's an absolutely mammoth question you've asked with many books written about it :)Barleycorn
Good answer, but all your first POST /players { "Name": "Murray" } should return 201 (Created) and Location header instead of 302.Kashakashden
Thanks for the comment - it may be a good call in 2021 and I've updated the codes (headers were always omitted for clarity). From a 2015/2016 PoV, I would've been grateful for a POST API to return anything other than 200, 403, 404 or 500. 302 was a code I might actually be able to convince a colleague to use :) 202 was super fancy. I know that's not ideal, but it is reality IME. That's also why I quoted the RFC - because people thought idempotence was obscure voodoo.Barleycorn
L
2

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"
        },
        ...
    ]
}
Larder answered 9/7, 2016 at 3:15 Comment(0)
O
-5
  1. /players (is a master resource)
  2. /teams/{id}/players (is a relationship resource, so it react diferent that 1)
  3. /memberships (is a relationship but semantically complicated)
  4. /players/memberships (is a relationship but semantically complicated)

I prefer 2

Ochone answered 21/6, 2014 at 5:5 Comment(6)
Perhaps I just don't understand the answer, but this post does not seem to answer the question.Turcotte
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.Jaimie
@IllegalArgument It is an answer and wouldn't make sense as a comment. However, it's not the greatest answer.Fining
This answer is difficult to follow and doesn't provide reasons.Spohr
This doesn't explain or answers the asked question at all.Westernize
This does answer the question in full if you understand the problem domain. Unfortunately, it seems like MoaLai isn't a native english speaker so can't elaborate as easily.Barleycorn

© 2022 - 2024 — McMap. All rights reserved.