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

时间:2011-06-12 20:44:00

标签: rest entity-relationship

想象一下,你有两个实体,玩家团队,玩家可以在多个团队中。在我的数据模型中,我有一个每个实体的表,以及一个用于维护关系的连接表。 Hibernate可以很好地处理这个问题,但是我如何在RESTful API中公开这种关系呢?

我可以想到几个方面。首先,我可能让每个实体都包含另一个实体的列表,因此Player对象将拥有它所属的Teams列表,并且每个Team对象都有一个属于它的Players列表。因此,要向团队添加播放器,您只需将播放器的表示形式发布到端点,例如POST /player或POST /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'
}

我能想到的另一种方法就是将关系本身作为一种资源来揭露。因此,要查看给定团队中所有玩家的列表,您可以执行GET /playerteam/team/{id}或类似的操作并获取PlayerTeam实体列表。要将玩家添加到团队,请使用适当构建的PlayerTeam实体作为有效负载PO​​ST /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'        
]

最佳做法是什么?

7 个答案:

答案 0 :(得分:237)

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

  1. REST是关于制作可进化的系统,如果没有别的。此时,您可能只关心某个玩家是否在特定团队中,但是在将来的某个时刻,您 希望用更多数据来注释该关系:他们已经存在了多长时间在该团队中,他们将他们介绍给那个团队,他们的教练是在那个团队中等等。
  2. REST依赖于缓存效率,这需要考虑缓存原子性和失效。如果您将新实体发布到/teams/3/players/,该列表将失效,但您不希望备用网址/players/5/teams/保持缓存状态。是的,不同的缓存将包含不同年龄的每个列表的副本,并且我们无法做很多事情,但我们至少可以通过限制我们需要使实体无效的实体数量来最小化用户POST更新的混淆在他们的客户端的本地缓存中只有一个 /memberships/98745(参见Helland对Life beyond Distributed Transactions中“备用索引”的讨论,以获得更详细的讨论)。
  3. 您只需选择/players/5/teams/teams/3/players(但不能同时选择两者),即可实现上述2点。让我们假设前者。但是,在某些时候,您需要保留/players/5/teams/以获取当前成员资格列表,并且能够在某处引用过去成员资格。将/players/5/memberships/列为指向/memberships/{id}/资源的超链接,然后您可以根据需要添加/players/5/past_memberships/,而无需打破每个会员资源的所有书签。这是一般概念;我相信你可以想象其他类似的期货更适用于你的具体情况。

答案 1 :(得分:111)

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

关系ID可能应该基于UUID或同样长且随机的内容,无论您为团队和玩家使用何种类型的ID。这将允许您为关系的每一端使用相同的UUID作为ID组件,而不必担心冲突(小整数具有这种优势)。如果这些会员关系具有除了与玩家和团队双向关联的事实以外的任何属性,他们应该拥有独立于玩家和团队的自己的身份;然后,玩家»团队视图(/player/{id}/teams)上的GET可以进行HTTP重定向到双向视图(/player/{playerID}/teams/{teamID})。

我建议您使用XLink /memberships/{uuid}属性在您返回的任何XML文档中编写链接(如果您恰好正在生成XML)。

答案 2 :(得分:54)

我会将这种关系映射到子资源,一般设计/遍历将是:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

在Restful-terms中,它不仅没有考虑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关系。

答案 3 :(得分:15)

现有的答案并非解释一致性和幂等性的作用 - 这会激发他们对UUIDs / ID的随机数和PUT而不是{的随机数的建议{1}}。

如果我们考虑的情况是我们有一个简单的场景,例如" 向团队添加新玩家",我们会遇到一致性问题。

因为玩家不存在,我们需要:

POST

但是,如果客户端操作在POST /players { "Name": "Murray" } //=> 302 /players/5 POST /teams/1/players/5 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 中有一个孤立的重复播放器。

要解决此问题,我们可能会编写自定义恢复代码,以检查与某些自然密钥匹配的孤立播放器(例如/players/5)。这是需要测试的自定义代码,花费更多的金钱和时间等等

为避免需要自定义恢复代码,我们可以实施Name而不是PUT

来自RFC

  

POST的意图是幂等的

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

这样我们就可以重新运行Id /playersPUT /memberships而不会产生任何后果:

PUT

一切都很好,除了重试部分失败之外,我们不需要做任何其他事情。

这更多是对现有答案的补充,但我希望它能让我们了解ReST的灵活性和可靠性的大局。

答案 4 :(得分:5)

我的首选解决方案是创建三个资源: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,因为返回的资源没有关于关系的信息,只有它自己的资源。

要处理n-n关系,请调用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. 查找玩家具有 id = playerId 的所有 TeamsPlayers 。 (GET /TeamsPlayers?player={playerId}
  2.   
  3. 循环返回的 TeamsPlayers
  4.   
  5. 使用从 TeamsPlayers 获得的 teamId ,调用GET /Teams/{teamId}并存储返回的数据
  6.   
  7. 循环结束后。返回循环中的所有团队。
  8.   

致电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资源都会自动更新。

答案 5 :(得分: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"
        },
        ...
    ]
}

答案 6 :(得分:-3)

  1. / players(是主要资源)
  2. / teams / {id} / players(是一种关系资源,所以它反应不同1)
  3. / membership(是一种关系,但在语义上很复杂)
  4. / players / membership(是一种关系,但在语义上很复杂)
  5. 我更喜欢2