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

假设你有两个实体,球员团队,玩家可以在多个队伍中。在我的数据模型中,每个实体都有一个表,还有一个连接表来维护这些关系。Hibernate很好地处理了这个问题,但是我如何在宁静的 API中公开这个关系呢?

我能想到几种方法。首先,我可能让每个实体都包含另一个实体的列表,所以一个Player对象将有一个它所属的Teams列表,而每个Team对象将有一个属于它的Player列表。因此,要将一个玩家添加到一个团队中,你只需将玩家的表示帖子到一个端点,例如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实体的列表。要将一名球员添加到球队,POST /playerteam,并将适当构建的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'
]

对此的最佳实践是什么?

118793 次浏览

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

# team resource
/teams/{teamId}


# players resource
/players/{playerId}


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

在rest术语中,它有助于不考虑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将球员分配给球队,而是使用,它更好地处理了球员和球队的n:n关系。

在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})。

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

创建一个单独的/memberships/资源集。

  1. REST是关于创造可进化的系统。在这个时候,你可能只关心一个给定的球员在一个给定的球队,但是在未来的某个时候,你想要用更多的数据来注释这个关系:他们在那支球队呆了多久,谁推荐他们到那支球队,他们在那支球队的教练是谁,等等等等。
  2. REST依赖于缓存来提高效率,这需要考虑缓存原子性和无效性。如果你POST一个新的实体到/teams/3/players/,这个列表将失效,但你不希望备用URL /players/5/teams/保留缓存。是的,不同的缓存会有不同年龄的每个列表的副本,对此我们可以做的不多,但我们至少可以通过限制我们需要在客户端本地缓存中使其失效的实体数量到/memberships/98745只有一个来最大限度地减少用户发布更新的困惑(有关更详细的讨论,请参阅Helland在分布式事务之外的生活中对“备用索引”的讨论)。
  3. 你可以通过简单地选择/players/5/teams/teams/3/players(但不能两者都选)来实现以上两点。我们假设是前者。然而,在某些情况下,你会希望为当前的成员列表保留/players/5/teams/,并且能够在某处引用过去的成员。让/players/5/memberships/成为指向/memberships/{id}/资源的超链接列表,然后你可以在你喜欢的时候添加/players/5/past_memberships/,而不必破坏每个人的会员资源的书签。这是一个普遍的概念;我相信你可以想象其他类似的未来,更适用于你的具体情况。
  1. /玩家(是主资源)
  2. /teams/{id}/players(是一个关系资源,所以它的反应与1不同)
  3. /成员关系(是一种关系,但语义复杂)
  4. /玩家/会员(是一种关系,但语义复杂)

我更喜欢2

我知道这个问题有一个公认的答案,但是,下面是我们如何解决之前提出的问题:

比方说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"
},
...
]
}

现有的答案并没有解释一致性和幂等性的作用——这促使他们推荐UUIDs/随机数作为id和PUT而不是POST

如果我们考虑一个简单的场景,比如“向团队中添加一名新玩家"”,我们就会遇到一致性问题。

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

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

然而,如果客户端操作在POST/players之后失败,我们已经创建了一个不属于球队的球员:

POST /players { "Name": "Murray" } //=> 201 /players/5
// *client failure*
// *client retries naively*
POST /players { "Name": "Murray" } //=> 201 /players/6
POST /teams/1/players/6

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

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

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

RFC:

PUT的意图是幂等的

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

这允许我们重新运行/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有多灵活和可靠的大背景下。

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

因此,要获取一个球队的所有球员,只需转到Teams资源,并通过调用GET /Teams/{teamId}/Players获取其所有球员。

另一方面,要获取球员参加过的所有球队,请获取Players. c中的Teams资源。GET /Players/{playerId}/Teams打电话。

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

注意,在这个解决方案中,当你调用GET /Players/{playerId}/Teams时,你会得到一个包含Teams资源的数组,这与你调用GET /Teams/{teamId}时得到的资源完全相同。反过来也遵循同样的原则,当调用GET /Teams/{teamId}/Players时,你会得到一个Players资源数组。

在这两个调用中,都不会返回关于关系的信息。例如,不返回contractStartDate,因为返回的资源没有关于关系的信息,只有关于它自己的资源。

要处理n-n关系,调用GET /Players/{playerId}/TeamsPlayersGET /Teams/{teamId}/TeamsPlayers。这些调用返回精确资源TeamsPlayers

这个TeamsPlayers资源有idplayerIdteamId属性,以及其他一些描述关系的属性。此外,它还具有处理这些问题所需的方法。GET, POST, PUT, DELETE等将返回,包括,更新,删除关系资源。

TeamsPlayers资源实现了一些查询,比如GET /TeamsPlayers?player={playerId}返回由{playerId}标识的玩家拥有的所有TeamsPlayers关系。按照同样的思路,使用GET /TeamsPlayers?team={teamId}返回在{teamId}队中参加过比赛的所有TeamsPlayers。 在任意一个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. 循环返回的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资源都会自动更新。