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

想象一下,你有两个实体, 玩家团队 ,玩家可以在多个团队。 在我的数据模型中,我为每个实体都有一个表,并且有一个连接表来维护关系。 Hibernate在处理这个问题上很好,但我怎样才能在RESTful API中公开这种关系呢?

我可以想到一些方法。 首先,我可能让每个实体都包含另一个实体的列表,因此一个Player对象将拥有它所属的团队列表,并且每个团队对象都有一个属于它的玩家列表。 所以要添加一个玩家到一个团队,你只需要将玩家的表示发送到一个端点,比如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实体列表。 要添加一个玩家到一个团队,POST /playerteam与一个适当build立的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' ] 

这个最好的做法是什么?

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

关系ID可能应该基于UUID或者同样长和随机的东西,而不pipe你用于团队和玩家的任何types的ID。 这样可以让你在关系的每一端使用与ID组件相同的UUID而不用担心碰撞(小整数没有这个优点)。 如果这些会员关系具有任何其他属性,而不是将玩家和团队以双向的方式联系起来,那么他们应该拥有自己的独立于玩家和团队的身份; ( /player/{playerID}/teams/{teamID} )可以执行HTTPredirect到双向视图( /memberships/{uuid} )。

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

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

  1. 如果没有其他的东西,REST就是要制作可演化的系统。 在这个时候,你可能只关心一个给定的球员是否在一个给定的球队,但是在将来的某个时刻,你想用更多的数据来注释这个关系:他们在球队上的时间有多长,到那个队,他们的教练是谁,还是在那个队里等等
  2. REST取决于caching的效率,这需要考虑cachingprimefaces性和失效。 如果您将一个新的实体发布到/teams/3/players/ ,那么这个列表将失效,但是您不希望替代的URL /players/5/teams/保持被caching。 是的,不同的caching会有不同年龄的每个列表的副本,我们可以做的并不多,但是我们至less可以通过限制我们需要失效的实体的数量来最小化用户在POST之后的混淆。在他们客户的本地caching中,只有一个/memberships/98745 (参见Helland关于“ 分布式事务之外的生活中的替代索引”的讨论以获得更详细的讨论)。
  3. 你可以通过select/players/5/teams/teams/3/players (但不是两者)来实现上述2点。 我们假设前者。 然而,在某些情况下,您将要保留/players/5/teams/来获取当前成员资格的列表,但能够在某处引用过去的成员资格。 将/players/5/memberships/超链接列表添加到/memberships/{id}/ resources中,然后您可以随时添加/players/5/past_memberships/ ,而无需为每个成员资源打破每个人的书签。 这是一个普遍的概念; 我相信你可以想象其他类似的期货更适合你的具体情况。

我会将这种关系与子资源进行映射,那么通用devise/遍历就是:

# team resource /teams/{teamId} # players resource /players/{playerId} # teams/players subresource /teams/{teamId}/players/{playerId}
# 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
# 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来更好地处理你和球员之间的关系。

现有的答案没有解释一致性和幂等性的angular色 – 这激发了他们对UUIDs /随机数的ID和PUT而不是POST

如果我们考虑一个简单的情况,比如“ 给团队添加新玩家 ”,我们会遇到一致性问题。

由于玩家不存在,我们需要:

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

但是,如果客户端操作在POST /players后失败,我们创build了一个不属于团队的播放器:

 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 )。 这是需要testing的定制代码,花费更多的金钱和时间等等

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

从RFC :

PUT的意图是幂等的

对于一个操作是幂等的,它需要排除外部数据,如服务器生成的id序列。 这就是为什么人们一起为Id推荐PUTUUID

这允许我们重新运行/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的灵活性和可靠性。

我知道这个问题有一个被接受的答案,但是,这里是我们如何解决以前提出的问题:

我们来说说吧

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

作为例子,下面的所有结果都会导致相同的效果,而不需要同步,因为它们是在单个资源上完成的:

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

现在如果我们想更新一个团队的多个成员资格,我们可以做如下(具有适当的validation):

 PUT /membership/teams/team1/ { membership: [ { teamId: "team1" playerId: "player1" }, { teamId: "team1" playerId: "player2" }, ... ] } 
  1. /播放器(是主资源)
  2. / teams / {id} / players(是一个关系资源,所以它的反应不同于1)
  3. /成员(是一种关系,但语义复杂)
  4. /玩家/会员(是一种关系,但语义复杂)

我更喜欢2