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

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

对此的最佳实践是什么?


当前回答

/玩家(是主资源) /teams/{id}/players(是一个关系资源,所以它的反应与1不同) /成员关系(是一种关系,但语义复杂) /玩家/会员(是一种关系,但语义复杂)

我更喜欢2

其他回答

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

# 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将球员分配到球队,而是使用PUT,它可以更好地处理球员和球队的n:n关系。

现有的答案并没有解释一致性和等幂的作用——这促使他们推荐uuid /随机数作为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中有一个孤立的副本播放器。

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

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

来自RFC:

PUT的意图是等幂的

对于幂等的操作,它需要排除外部数据,比如服务器生成的id序列。这就是为什么人们同时推荐PUT和uuid作为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有多灵活和可靠的大背景下。

/玩家(是主资源) /teams/{id}/players(是一个关系资源,所以它的反应与1不同) /成员关系(是一种关系,但语义复杂) /玩家/会员(是一种关系,但语义复杂)

我更喜欢2

我的首选解决方案是创建三个资源:玩家、团队和TeamsPlayers。

因此,要获取一个团队的所有球员,只需访问Teams资源并通过调用get /Teams/{teamId}/ players来获取其所有球员。

另一方面,要获取球员参加过的所有球队,请获取球员中的teams资源。调用GET /Players/{playerId}/Teams。

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

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

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

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

这个TeamsPlayers资源有id、playerId、teamId属性,以及其他一些描述关系的属性。此外,它还具有处理这些问题所需的方法。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的工作原理如下:

找到所有队员id = playerId的TeamsPlayers。(获得/ TeamsPlayers ?球员= {playerId}) 循环返回的TeamsPlayers 使用从TeamsPlayers获得的teamId,调用GET /Teams/{teamId}并存储返回的数据 在循环结束之后。返回所有进入循环的团队。

当调用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简单明了。

无论何时创建、更新或删除关系,玩家和团队资源都会自动更新。

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

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