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

Ric*_*ker 276 rest entity-relationship

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

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

我能想到的另一种方法就是将关系本身作为一种资源来揭露.因此,要查看给定团队中所有玩家的列表,您可以执行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'        
]
Run Code Online (Sandbox Code Playgroud)

这是什么最好的做法?

fum*_*chu 250

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

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

  • 第1点和第2点是完美的解释,谢谢,如果有人在现实生活经验中有更多的肉,这将有助于我. (10认同)
  • 嗨fumanchu.问题:在其余端点/成员资格/ 98745中,该网址末尾的数字代表什么?它是会员的唯一ID吗?如何与成员资格端点进行交互?要添加播放器,将发送包含{team:3,player:6}的有效负载的POST,从而创建两者之间的链接?GET怎么样?你会发送GET到/ memberships?player =和/ membersihps?team =获得结果?那是个主意吗?我错过了什么吗?(我正在尝试学习休息端点)在这种情况下,会员资格/ 98745中的id 98745是否真的有用? (6认同)
  • 如果您认为这里的“Membership”相当于“TeamPlayers”,如上面所述/sf/answers/3622374801/。这两个答案是相似的、互补的,而且都很棒。但是,对于复杂的 API,我建议不要为同一想法使用太多名称(例如,为用户会员资格保留可用的会员资格)。api/teamplayers/<id> 完全没问题。 (3认同)
  • 最好最简单的回答IMO谢谢!拥有两个端点并使它们保持同步有许多复杂性. (2认同)

Don*_*ows 118

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

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

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


man*_*ana 60

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

# 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关系.

  • 如果team_player有其他信息,如状态等,该怎么办?我们在你的模型中代表它的位置?我们可以将它推广到资源,并为它提供网址,就像游戏/,播放器/ (17认同)
  • 我同意你的映射,但有一个问题.这是个人观点的问题,但你怎么看待POST /球队/ 1 /球员以及你为什么不使用它?你觉得这种做法有什么不利/误导吗? (2认同)
  • POST不是幂等的,即如果你执行POST /队列/ 1 /队员n次,你将改变n次/队员/ 1.但是将球员移动到/队伍/ 1次并不会改变球队的状态,因此使用PUT更为明显. (2认同)

Set*_*eth 19

现有的答案并没有解释一致性和幂等性的作用 - 这会激发他们UUIDs对ID的随机数的推荐,而PUT不是POST.

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

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

POST /players { "Name": "Murray" } //=> 302 /players/5
POST /teams/1/players/5
Run Code Online (Sandbox Code Playgroud)

但是,如果客户端操作在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
Run Code Online (Sandbox Code Playgroud)

现在我们有一个孤立的重复玩家/players/5.

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

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

来自RFC:

意图PUT是幂等的

对于幂等的操作,它需要排除外部数据,例如服务器生成的id序列.这就是为什么人们都推荐PUTUUID对于s 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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 键盘上的滚动面 - 23lkr 没有什么特别的...除了它没有顺序或有意义之外 (2认同)

Har*_*edo 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返回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',
}
Run Code Online (Sandbox Code Playgroud)

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

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