如何在 RESTful API 中处理多对多关系?
假设您有两个实体,玩家和团队,其中玩家可以属于多个团队。在我的数据模型中,每个实体都有一个表,还有一个用于维护关系的连接表。 Hibernate 可以很好地处理这个问题,但是我如何在 < 中公开这种关系href="https://en.wikipedia.org/wiki/Representational_state_transfer#RESTful_web_services" rel="noreferrer">RESTful API?
我可以想出几种方法。首先,我可能让每个实体都包含另一个实体的列表,因此 Player 对象将具有它所属的 Teams 列表,并且每个 Team 对象将具有属于它的 Players 列表。因此,要将玩家添加到团队中,您只需将玩家的代表 POST 到端点,例如 POST /player
或 POST /team
,并使用适当的对象作为请求的负载。这对我来说似乎是最“RESTful”的,但感觉有点奇怪。
/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 实体列表。要将玩家添加到团队中,请使用适当构建的 PlayerTeam 实体作为有效负载进行 POST /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'
]
对此的最佳实践是什么?
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
我知道这个问题有一个标记为已接受的答案,但是,我们可以通过以下方式解决之前提出的问题:
以 PUT
为例,以下内容都将产生相同的效果,而无需同步,因为它们在单个资源上完成:
现在,如果我们想要更新一个团队的多个成员资格,我们可以执行以下操作(经过适当的验证):
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
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:
now if we want to update multiple memberships for one team we could do as follows (with proper validations):
我更喜欢2
I prefer 2
创建一组单独的
/memberships/
资源。/teams/3/players/
,该列表将会失效,但您不希望备用 URL/players/5/teams/
保持缓存。是的,不同的缓存将拥有不同年龄的每个列表的副本,对此我们无能为力,但我们至少可以通过限制需要无效的实体数量来最大程度地减少用户发布更新的混乱在其客户端的本地缓存中,只有一个,位于/memberships/98745
(请参阅 Helland 在 分布式事务之外的生活以获取更详细的讨论)。/players/5/teams
或/teams/3/players
(但不能同时选择两者)即可实现上述两点。我们假设是前者。然而,在某些时候,您可能希望保留/players/5/teams/
作为当前会员资格列表,但又能够参考过去的会员资格 某处的会员资格。将/players/5/memberships/
制作为/memberships/{id}/
资源的超链接列表,然后您可以添加/players/5/past_memberships /
当您愿意时,无需破坏每个人对个人会员资源的书签。这是一个一般概念;我相信您可以想象其他更适合您的具体情况的类似未来。Make a separate set of
/memberships/
resources./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)./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.在 RESTful 接口中,您可以通过将这些关系编码为链接来返回描述资源之间关系的文档。因此,一个团队可以说拥有一个文档资源 (
/team/{id}/players
),它是指向球员的链接列表 (/player/{id}
) >)在团队中,玩家可以拥有一个文档资源(/player/{id}/teams
),它是指向该玩家所属团队的链接列表。漂亮又对称。您可以轻松地对该列表进行地图操作,甚至可以为某个关系提供自己的 ID(可以说它们有两个 ID,具体取决于您是考虑团队优先还是玩家优先的关系),如果这会让事情变得更容易。唯一棘手的一点是,如果您从一端删除关系,则必须记住从另一端删除该关系,但要通过使用底层数据模型严格处理此问题,然后让 REST 接口成为该模型将使这变得更容易。关系 ID 可能应该基于 UUID 或同样长且随机的东西,无论您为团队和玩家使用什么类型的 ID。这样您就可以使用相同的 UUID 作为关系两端的 ID 组件,而不必担心冲突(小整数不有这个优势)。如果这些成员关系除了以双向方式关联球员和球队这一事实之外还具有任何属性,那么它们应该拥有独立于球员和球队的自己的身份;然后,玩家»团队视图 (
/player/{playerID}/teams/{teamID}
) 上的 GET 可以执行 HTTP 重定向到双向视图 (/memberships/{uuid}< /代码>)。
我建议使用 XLink< 在您返回的任何 XML 文档中编写链接(当然,如果您碰巧生成 XML) /a>
xlink:href
属性。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.我会将这种关系与子资源进行映射,一般设计/遍历将是:
用 RESTful 术语来说,它有助于不考虑 SQL 和联接,而是更多地考虑集合、子集合和遍历。
一些示例:
如您所见,我不使用 POST 将玩家分配到团队中,而是使用 PUT,它可以更好地处理玩家和团队之间的 n:n 关系。
I would map such relationship with sub-resources, general design/traversal would then be:
In RESTful-terms it helps a lot in not thinking of SQL and joins, but more into collections, sub-collections and traversal.
Some examples:
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.
我的首选解决方案是创建三个资源:
Players
、Teams
和TeamsPlayers
。因此,要获取一支球队的所有球员,只需转到
Teams
资源并通过调用GET /Teams/{teamId}/Players
获取其所有球员。另一方面,要获取玩家参加过的所有球队,请获取
Players
中的Teams
资源。调用GET /Players/{playerId}/Teams
。并且,要获取多对多关系,请调用
GET /Players/{playerId}/TeamsPlayers
或GET /Teams/{teamId}/TeamsPlayers
。请注意,在此解决方案中,当您调用
GET /Players/{playerId}/Teams
时,您将获得一组Teams
资源,这与您获得的资源完全相同当您调用GET /Teams/{teamId}
时。反之亦然,调用GET /Teams/{teamId}/Players
时会得到一个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}
返回所有在{teamId}
队伍中比赛过的TeamsPlayers
。在任一
GET
调用中,都会返回资源TeamsPlayers
。返回与该关系相关的所有数据。当调用
GET /Players/{playerId}/Teams
(或GET /Teams/{teamId}/Players
)时,资源Players
(或Teams
)调用TeamsPlayers
使用查询过滤器返回相关团队(或玩家)。GET /Players/{playerId}/Teams
的工作方式如下:在调用
GET /Teams/{teamId}/Players
时,您可以使用相同的算法获取团队中的所有玩家,但交换团队和玩家。我的资源将如下所示:
该解决方案仅依赖于 REST 资源。尽管可能需要一些额外的调用才能从球员、球队或其关系中获取数据,但所有 HTTP 方法都很容易实现。 POST、PUT、DELETE 简单明了。
每当创建、更新或删除关系时,
Players
和Teams
资源都会自动更新。My preferred solution is to create three resources:
Players
,Teams
andTeamsPlayers
.So, to get all the players of a team, just go to
Teams
resource and get all its players by callingGET /Teams/{teamId}/Players
.On the other hand, to get all the teams a player has played, get the
Teams
resource within thePlayers
. CallGET /Players/{playerId}/Teams
.And, to get the many-to-many relationship call
GET /Players/{playerId}/TeamsPlayers
orGET /Teams/{teamId}/TeamsPlayers
.Note that, in this solution, when you call
GET /Players/{playerId}/Teams
, you get an array ofTeams
resources, that is exactly the same resource you get when you callGET /Teams/{teamId}
. The reverse follows the same principle, you get an array ofPlayers
resources when callGET /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
orGET /Teams/{teamId}/TeamsPlayers
. These calls return the exactly resource,TeamsPlayers
.This
TeamsPlayers
resource hasid
,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, likeGET /TeamsPlayers?player={playerId}
to return allTeamsPlayers
relationships the player identified by{playerId}
has. Following the same idea, useGET /TeamsPlayers?team={teamId}
to return all theTeamsPlayers
that have played in the{teamId}
team.In either
GET
call, the resourceTeamsPlayers
is returned. All the data related to the relationship is returned.When calling
GET /Players/{playerId}/Teams
(orGET /Teams/{teamId}/Players
), the resourcePlayers
(orTeams
) callsTeamsPlayers
to return the related teams (or players) using a query filter.GET /Players/{playerId}/Teams
works like this: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:
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
andTeams
resources are automatically updated.现有的答案没有解释一致性和幂等性的作用 - 这促使他们推荐
UUID
/ID 的随机数和PUT
而不是发布
。如果我们考虑这样一个简单的场景,例如“向团队添加新玩家”,我们就会遇到一致性问题。
因为播放器不存在,所以我们需要:
但是,如果在
POST
到/players
后客户端操作失败,我们会创建一个不存在的播放器属于一个团队:现在我们在
/players/5
中有一个孤立的重复玩家。为了解决这个问题,我们可以编写自定义恢复代码来检查与某些自然键(例如
Name
)匹配的孤立玩家。这是需要测试的自定义代码,需要花费更多的金钱和时间等。为了避免需要自定义恢复代码,我们可以实现
PUT
而不是POST
。来自 RFC:
对于幂等的操作,它需要排除外部数据,例如服务器生成的 id 序列。这就是为什么人们同时推荐
PUT
和UUID
作为Id
的原因。这使我们能够重新运行
/players
PUT
和/memberships
PUT
而不会产生任何后果:一切都很好,我们不需要做任何事情,只需重试部分失败即可。
这更多的是对现有答案的补充,但我希望它能将它们置于 ReST 的灵活性和可靠性的大背景中。
The existing answers don't explain the roles of consistency and idempotency - which motivate their recommendations of
UUIDs
/random numbers for IDs andPUT
instead ofPOST
.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:
However, should the client operation fail after the
POST
to/players
, we've created a player that doesn't belong to a team: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 etcTo avoid needing custom recovery code, we can implement
PUT
instead ofPOST
.From the RFC:
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
andUUID
s forId
s together.This allows us to rerun both the
/players
PUT
and the/memberships
PUT
without consequences: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.