如何在RESTful API中处理多对多关系?


288

假设您有2个实体,PlayerTeam,其中玩家可以在多个团队中。在我的数据模型中,我为每个实体都有一个表,以及一个用于维护关系的联接表。Hibernate很擅长处理此问题,但是如何在RESTful API中公开这种关系?

我可以想到几种方法。首先,我可能让每个实体包含另一个实体的列表,因此Player对象将具有其所属的团队的列表,并且每个Team对象将具有一个属于其的Player的列表。因此,要将玩家添加到团队中,您只需将玩家的表示发布到端点,例如POST /player/team带有适当对象作为请求负载的POST 。对我来说,这似乎是最“ 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使用适当构建的PlayerTeam实体作为有效载荷进行POST 。

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

最佳做法是什么?

Answers:


129

在RESTful界面中,您可以通过将资源之间的关系编码为链接来返回描述资源之间关系的文档。因此,可以说一个团队拥有文档资源(/team/{id}/players),该文档资源是指向/player/{id}团队中玩家()的链接的列表,而玩家可以拥有文档资源()。/player/{id}/teams),即指向该玩家所属团队的链接列表。尼斯和对称。您可以轻松轻松地对该列表上的地图操作进行操作,甚至可以给关系指定一个自己的ID(可以说,他们有两个ID,具体取决于您考虑的是团队优先还是玩家优先),这样可以使事情变得更容易。唯一棘手的一点是,如果您从一端删除了该关系,则还必须记住还要从另一端删除该关系,但是要通过使用基础数据模型然后使用REST接口作为视图来严格处理此关系。该模型将使其变得更容易。

关系ID可能应该基于UUID或同样长且随机的某种值,而不管您用于团队和球员的ID的类型如何。这样一来,您就可以为关系的每一端使用与ID组件相同的UUID,而不必担心冲突(小整数没有这种优势)。如果这些成员关系除了具有以双向方式关联球员和团队的事实之外,还具有其他属性,则它们应具有独立于球员和团队的身份;/player/{playerID}/teams/{teamID}然后,玩家»团队视图()上的GET 可以执行HTTP重定向到双向视图(/memberships/{uuid})。

我建议使用XLink xlink:href属性在返回的任何XML文档中编写链接(如果您碰巧正在生成XML)。


265

制作一组单独的/memberships/资源。

  1. REST就是要打造可演化的系统。目前,您可能只关心给定的球员在给定的团队中,但是在将来的某个时候,您希望使用更多数据来注释这种关系:他们在该团队中待了多长时间,谁转介了他们给那个团队,他们的教练在那个团队中是谁,等等。
  2. REST依靠缓存来提高效率,这需要考虑缓存的原子性和无效性。如果您将新实体发布到/teams/3/players/该列表,则该实体将失效,但是您不希望备用URL /players/5/teams/保持高速缓存。是的,不同的缓存将具有不同年龄的每个列表的副本,对此我们无能为力,但是我们至少可以通过限制需要使无效的实体数量来最小化用户POST更新的混乱在他们的客户的本地缓存中只有一个/memberships/98745只有一个)(有关详细讨论,请参见Helland在“分布式事务之外的生命 ”中对“替代索引”的讨论)。
  3. 您可以通过选择/players/5/teams/teams/3/players(但不能同时选择两者)来实现以上两点。让我们假设前者。但是,在某个时候,您将希望保留当前成员资格/players/5/teams/的列表,但仍能够在某处引用过去的成员资格。让超链接到列表中的资源,然后你可以添加当你喜欢,而不必打破大家的书签为个人会员资源。这是一个一般概念;我相信您可以想象其他类似的期货更适合您的特定情况。/players/5/memberships//memberships/{id}//players/5/past_memberships/

11
很好地解释了第1点和第2点,谢谢,如果在现实生活中有人为第3点增加了肉食,那对我有帮助。
Alain

2
最好和最简单的答案IMO谢谢!有两个端点并使它们保持同步会带来很多麻烦。
Venkat D.

7
嗨,满满。问题:在其余端点/ memberships / 98745中,URL末尾的数字代表什么?它是会员的唯一标识吗?一个人将如何与成员资格端点交互?要添加玩家,是否会发送{团队:3,玩家:6}包含有效负载的POST,从而在两者之间建立链接?GET呢?您会发送GET到/ membership?player =和/ membersihps?team =以获得结果吗?那是主意吗?我有什么想念的吗?(我正在尝试学习宁静的端点)在那种情况下,memberships / 98745中的ID 98745真的有用吗?
aruuuuu

@aruuuuu应该为关联的单独端点提供代理PK。通常,它也使生活变得更加轻松:/ memberships / {membershipId}。密钥(playerId,teamId)保持唯一,因此可以在具有这种关系的资源上使用:/ teams / {teamId} / players和/ players / {playerId} / teams。但是,并非总是在双方之间保持这种关系。例如,食谱和配料:您几乎不需要使用/ ingredients / {ingredientId} / recipes /。
亚历山大·帕拉玛丘克

65

我将这种关系与子资源映射,那么一般的设计/遍历将是:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

在Restful术语中,它在不考虑SQL和联接的情况下有很大帮助,但更多地用于集合,子集合和遍历。

一些例子:

# 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

如您所见,我不使用POST将球员安排到球队中,而是使用PUT来更好地处理您与球员和球队之间的n:n关系。


20
如果team_player具有状态等其他信息怎么办?我们在您的模型中代表什么?我们能否将其提升为一种资源并为其提供URL,就像游戏,玩家一样?
Narendra Kamma,2013年

嘿,快速提问只是为了确保我做对了:GET / teams / 1 / players / 3返回一个空的响应正文。唯一有意义的响应是200 vs404。GET / teams / 1 / players / 3不返回玩家实体的信息(名称,年龄等)。如果客户希望获得有关播放器的其他信息,则必须GET / players / 3。这一切正确吗?
Verdagon

2
我同意您的映射,但有一个问题。这是个人观点,但是您对POST / teams / 1 / players怎么看?为什么不使用它呢?您认为这种方法有什么缺点/误导性吗?
JakubKnejzlik 2014年

2
POST不是幂等的,即,如果您将POST / teams / 1 / players进行n次,则将更改n次/ teams / 1。但是将一名球员移动到/ teams / 1 n次不会改变球队的状态,因此使用PUT更为明显。
曼努埃尔·奥尔丹娜2014年

1
我假设@NarendraKamma只是status作为PUT请求中的参数发送?这种方法有不利之处吗?
Traxo

22

现有的答案并未解释一致性和幂等的作用-促使他们建议UUIDs使用ID 的/ random number PUT代替ID POST

如果我们考虑一种简单的情况,例如“ 将新玩家添加到团队,则会遇到一致性问题。

由于播放器不存在,因此我们需要:

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

但是,如果在POSTto 之后客户端操作失败/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

现在我们有一个孤立的重复播放器 /players/5

为了解决这个问题,我们可能会编写自定义恢复代码,以检查与某个自然键匹配的孤立玩家(例如 Name)。这是需要测试的自定义代码,花费更多的金钱和时间等

为了避免需要自定义恢复代码,我们就可以实现PUT的,而不是POST

RFC

的意图PUT是幂等的

为了使操作成为幂等,它需要排除外部数据,例如服务器生成的ID序列。这就是为什么人们同时推荐PUTUUID的原因Id的原因。

这使我们可以重新运行/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

一切都很好,我们只需要重试部分故障即可。

这更多地是现有答案的附录,但我希望它能将ReST的灵活性和可靠性看得更清楚。


在这个假设的终点,您23lkrjrqwlej从哪里得到的?
cbcoutinho

1
在键盘上滚动脸-23lkr ... gobbledegook没什么特别之处,只是它不是顺序的或有意义的
Seth

9

我首选的方案是创建三个资源:PlayersTeamsTeamsPlayers

因此,要获得团队的所有参与者,只需访问Teams资源并通过致电来获取其所有参与者GET /Teams/{teamId}/Players

另一方面,要获得玩家参与的所有团队,请在中获取Teams资源Players。呼叫GET /Players/{playerId}/Teams

并且,要获得多对多关系电话GET /Players/{playerId}/TeamsPlayersGET /Teams/{teamId}/TeamsPlayers

请注意,在此解决方案中,当您调用时GET /Players/{playerId}/Teams,您将获得一个Teams资源数组,这与您在调用时获得的资源完全相同GET /Teams/{teamId}。反向遵循相同的原理,Players当调用时,您将获得一组资源GET /Teams/{teamId}/Players

在两个调用中,都不返回有关该关系的信息。例如,不contractStartDate返回no ,因为返回的资源没有有关该关系的信息,而只有其自己的资源。

要处理nn关系,请调用GET /Players/{playerId}/TeamsPlayersGET /Teams/{teamId}/TeamsPlayers。这些调用返回确切的资源TeamsPlayers

这种TeamsPlayers资源有idplayerIdteamId属性,以及一些其他人描述这种关系。而且,它具有处理它们的必要方法。GET,POST,PUT,DELETE等将返回,包含,更新,删除关系资源。

TeamsPlayers资源实现了一些查询,例如GET /TeamsPlayers?player={playerId}返回TeamsPlayers玩家所标识的所有关系{playerId}。遵循相同的想法,使用GET /TeamsPlayers?team={teamId}来返回团队中所有TeamsPlayers已参加的{teamId}比赛。在任一GET调用中,资源都TeamsPlayers被返回。返回与该关系有关的所有数据。

在致电GET /Players/{playerId}/Teams(或GET /Teams/{teamId}/Players)时,资源Players(或Teams)调用TeamsPlayers以使用查询过滤器返回相关的球队(或球员)。

GET /Players/{playerId}/Teams 像这样工作:

  1. 找到所有TeamsPlayers球员ID = playerId。(GET /TeamsPlayers?player={playerId}
  2. 循环返回的TeamsPlayers
  3. 使用从TeamsPlayers获得的teamId,调用GET /Teams/{teamId}并存储返回的数据
  4. 循环结束后。返回循环中的所有团队。

您可以在呼叫时使用相同的算法从团队中获取所有球员 GET /Teams/{teamId}/Players,,但交换团队和球员。

我的资源如下所示:

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

此解决方案仅依赖REST资源。尽管可能需要一些额外的调用才能从玩家,团队或其关系中获取数据,但所有HTTP方法都易于实现。POST,PUT,DELETE非常简单明了。

每当创建,更新或删除关系时PlayersTeams资源都会自动更新。


真的很有意义引进TeamsPlayers resource.Awesome
维杰-

最好的解释
戴安娜

1

我知道有一个答案被标记为接受该问题,但是,这是我们如何解决先前提出的问题:

假设是PUT

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

举例来说,以下所有操作都将产生相同的效果,而无需同步,因为它们是在单个资源上完成的:

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

现在,如果我们要为一个团队更新多个成员身份,我们可以执行以下操作(并进行适当的验证):

PUT    /membership/teams/team1/

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

-3
  1. / players(是主资源)
  2. / teams / {id} / players(是一种关系资源,因此对1的反应不同)
  3. /会员(是一种关系,但语义上很复杂)
  4. / players / memberships(是一种关系,但语义上很复杂)

我喜欢2


2
也许我只是不明白答案,但是这篇文章似乎并没有回答这个问题。
BradleyDotNET

这不能为问题提供答案。要批评或要求作者澄清,请在其帖子下方留下评论-您可以随时对自己的帖子发表评论,一旦您拥有足够的声誉,就可以在任何帖子中发表评论
非法论点

4
@IllegalArgument这一个答案,没有任何评论意义。但是,这不是最大的答案。
Qix-蒙尼卡(Monica)错误14'21

1
这个答案很难理解,也没有提供原因。
Venkat D.

2
这根本无法解释或回答所提问题。
Manjit Kumar '02
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.