假设你有两个实体,玩家和团队,玩家可以在多个团队中。在我的数据模型中,每个实体都有一个表,还有一个连接表来维护这些关系。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'
]

对此的最佳实践是什么?


当前回答

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

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

其他回答

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

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

我的首选解决方案是创建三个资源:玩家、团队和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简单明了。

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

In a RESTful interface, you can return documents that describe the relationships between resources by encoding those relationships as links. Thus, a team can be said to have a document resource (/team/{id}/players) that is a list of links to players (/player/{id}) on the team, and a player can have a document resource (/player/{id}/teams) that is a list of links to teams that the player is a member of. Nice and symmetric. You can the map operations on that list easily enough, even giving a relationship its own IDs (arguably they'd have two IDs, depending on whether you're thinking about the relationship team-first or player-first) if that makes things easier. The only tricky bit is that you've got to remember to delete the relationship from the other end as well if you delete it from one end, but rigorously handling this by using an underlying data model and then having the REST interface be a view of that model is going to make that easier.

Relationship IDs probably ought to be based on UUIDs or something equally long and random, irrespective of whatever type of IDs you use for teams and players. That will let you use the same UUID as the ID component for each end of the relationship without worrying about collisions (small integers do not have that advantage). If these membership relationships have any properties other than the bare fact that they relate a player and a team in a bidirectional fashion, they should have their own identity that is independent of both players and teams; a GET on the player»team view (/player/{playerID}/teams/{teamID}) could then do an HTTP redirect to the bidirectional view (/memberships/{uuid}).

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

制作一组单独的/成员/资源。

REST is about making evolvable systems if nothing else. At this moment, you may only care that a given player is on a given team, but at some point in the future, you will want to annotate that relationship with more data: how long they've been on that team, who referred them to that team, who their coach is/was while on that team, etc etc. REST depends on caching for efficiency, which requires some consideration for cache atomicity and invalidation. If you POST a new entity to /teams/3/players/ that list will be invalidated, but you don't want the alternate URL /players/5/teams/ to remain cached. Yes, different caches will have copies of each list with different ages, and there's not much we can do about that, but we can at least minimize the confusion for the user POST'ing the update by limiting the number of entities we need to invalidate in their client's local cache to one and only one at /memberships/98745 (see Helland's discussion of "alternate indices" in Life beyond Distributed Transactions for a more detailed discussion). You could implement the above 2 points by simply choosing /players/5/teams or /teams/3/players (but not both). Let's assume the former. At some point, however, you will want to reserve /players/5/teams/ for a list of current memberships, and yet be able to refer to past memberships somewhere. Make /players/5/memberships/ a list of hyperlinks to /memberships/{id}/ resources, and then you can add /players/5/past_memberships/ when you like, without having to break everyone's bookmarks for the individual membership resources. This is a general concept; I'm sure you can imagine other similar futures which are more applicable to your specific case.

现有的答案并没有解释一致性和等幂的作用——这促使他们推荐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有多灵活和可靠的大背景下。