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

发布于 2024-11-14 23:57:42 字数 1498 浏览 6 评论 0原文

假设您有两个实体,玩家团队,其中玩家可以属于多个团队。在我的数据模型中,每个实体都有一个表,还有一个用于维护关系的连接表。 Hibernate 可以很好地处理这个问题,但是我如何在 < 中公开这种关系href="https://en.wikipedia.org/wiki/Representational_state_transfer#RESTful_web_services" rel="noreferrer">RESTful API?

我可以想出几种方法。首先,我可能让每个实体都包含另一个实体的列表,因此 Player 对象将具有它所属的 Teams 列表,并且每个 Team 对象将具有属于它的 Players 列表。因此,要将玩家添加到团队中,您只需将玩家的代表 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 实体列表。要将玩家添加到团队中,请使用适当构建的 PlayerTeam 实体作为有效负载进行 POST /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'
]

对此的最佳实践是什么?

Imagine you have two entities, Player and Team, where players can be on multiple teams. In my data model, I have a table for each entity, and a join table to maintain the relationships. Hibernate is fine at handling this, but how might I expose this relationship in a RESTful API?

I can think of a couple of ways. First, I might have each entity contain a list of the other, so a Player object would have a list of Teams it belongs to, and each Team object would have a list of Players that belong to it. So to add a Player to a Team, you would just POST the player's representation to an endpoint, something like POST /player or POST /team with the appropriate object as the payload of the request. This seems the most "RESTful" to me, but feels a little weird.

/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'
}

The other way I can think of to do this would be to expose the relationship as a resource in its own right. So to see a list of all the players on a given team, you might do a GET /playerteam/team/{id} or something like that and get back a list of PlayerTeam entities. To add a player to a team, POST /playerteam with an appropriately built PlayerTeam entity as the payload.

/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'
]

What is the best practice for this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

苹果你个爱泡泡 2024-11-21 23:57:43

我知道这个问题有一个标记为已接受的答案,但是,我们可以通过以下方式解决之前提出的问题:

以 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"
        },
        ...
    ]
}

I know that there's an answer marked as accepted for this question, however, here is how we could solve the previously raised issues:

Let's say for PUT

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

As an example, the followings will all result in the same effect without a need for syncing because they are done on a single resource:

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

now if we want to update multiple memberships for one team we could do as follows (with proper validations):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}
深陷 2024-11-21 23:57:43
  1. /players (是主资源)
  2. /teams/{id}/players (是关系资源,因此它的反应与 1)
  3. /memberships (是关系但语义复杂)
  4. /players/memberships (是关系但语义复杂) )

我更喜欢2

  1. /players (is a master resource)
  2. /teams/{id}/players (is a relationship resource, so it react diferent that 1)
  3. /memberships (is a relationship but semantically complicated)
  4. /players/memberships (is a relationship but semantically complicated)

I prefer 2

情话已封尘 2024-11-21 23:57:42

创建一组单独的 /memberships/ 资源。

  1. 如果没有别的,REST 就是关于创建可进化的系统。此时,您可能只关心某个特定的玩家在某个特定的团队中,但在未来的某个时刻,您想要用更多数据来注释这种关系:他们加入了多长时间在该团队中,谁将他们推荐给该团队,他们的教练在该团队中是谁,等等。REST
  2. 依赖于缓存来提高效率,这需要考虑缓存原子性和失效。如果您将新实体发布到 /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 / 当您愿意时,无需破坏每个人对个人会员资源的书签。这是一个一般概念;我相信您可以想象其他更适合您的具体情况的类似未来。

Make a separate set of /memberships/ resources.

  1. 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.
  2. 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).
  3. 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.
小猫一只 2024-11-21 23:57:42

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

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

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}).

I recommend writing links in any XML documents you return (if you happen to be producing XML of course) using XLink xlink:href attributes.

情绪失控 2024-11-21 23:57:42

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

# 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

如您所见,我不使用 POST 将玩家分配到团队中,而是使用 PUT,它可以更好地处理玩家和团队之间的 n:n 关系。

I would map such relationship with sub-resources, general design/traversal would then be:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

In RESTful-terms it helps a lot in not thinking of SQL and joins, but more into collections, sub-collections and traversal.

Some examples:

# 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

As you see, I don't use POST for placing players to teams, but PUT, which handles your n:n relationship of players and teams better.

っ左 2024-11-21 23:57:42

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

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

另一方面,要获取玩家参加过的所有球队,请获取 Players 中的 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,因为返回的资源没有关于关系的信息,只有关于它自己的资源的信息。

要处理 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} 返回所有在 {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 资源都会自动更新。

My preferred solution is to create three resources: Players, Teams and TeamsPlayers.

So, to get all the players of a team, just go to Teams resource and get all its players by calling GET /Teams/{teamId}/Players.

On the other hand, to get all the teams a player has played, get the Teams resource within the Players. Call GET /Players/{playerId}/Teams.

And, to get the many-to-many relationship call GET /Players/{playerId}/TeamsPlayers or GET /Teams/{teamId}/TeamsPlayers.

Note that, in this solution, when you call GET /Players/{playerId}/Teams, you get an array of Teams resources, that is exactly the same resource you get when you call GET /Teams/{teamId}. The reverse follows the same principle, you get an array of Players resources when call GET /Teams/{teamId}/Players.

In either calls, no information about the relationship is returned. For example, no contractStartDate is returned, because the resource returned has no info about the relationship, only about its own resource.

To deal with the n-n relationship, call either GET /Players/{playerId}/TeamsPlayers or GET /Teams/{teamId}/TeamsPlayers. These calls return the exactly resource, TeamsPlayers.

This TeamsPlayers resource has id, playerId, teamId attributes, as well as some others to describe the relationship. Also, it has the methods necessary to deal with them. GET, POST, PUT, DELETE etc that will return, include, update, remove the relationship resource.

The TeamsPlayers resource implements some queries, like GET /TeamsPlayers?player={playerId} to return all TeamsPlayers relationships the player identified by {playerId} has. Following the same idea, use GET /TeamsPlayers?team={teamId} to return all the TeamsPlayers that have played in the {teamId} team.
In either GET call, the resource TeamsPlayers is returned. All the data related to the relationship is returned.

When calling GET /Players/{playerId}/Teams (or GET /Teams/{teamId}/Players), the resource Players (or Teams) calls TeamsPlayers to return the related teams (or players) using a query filter.

GET /Players/{playerId}/Teams works like this:

  1. Find all TeamsPlayers that the player has id = playerId. (GET /TeamsPlayers?player={playerId})
  2. Loop the returned TeamsPlayers
  3. Using the teamId obtained from TeamsPlayers, call GET /Teams/{teamId} and store the returned data
  4. After the loop finishes. Return all teams that were got in the loop.

You can use the same algorithm to get all players from a team, when calling GET /Teams/{teamId}/Players, but exchanging teams and players.

My resources would look like this:

/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',
}

This solution relies on REST resources only. Although some extra calls may be necessary to get data from players, teams or their relationship, all HTTP methods are easily implemented. POST, PUT, DELETE are simple and straightforward.

Whenever a relationship is created, updated or deleted, both Players and Teams resources are automatically updated.

赠佳期 2024-11-21 23:57:42

现有的答案没有解释一致性和幂等性的作用 - 这促使他们推荐 UUID/ID 的随机数和 PUT 而不是发布

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

因为播放器不存在,所以我们需要:

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 序列。这就是为什么人们同时推荐 PUTUUID 作为 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 的灵活性和可靠性的大背景中。

The existing answers don't explain the roles of consistency and idempotency - which motivate their recommendations of UUIDs/random numbers for IDs and PUT instead of POST.

If we consider the case where we have a simple scenario like "Add a new player to a team", we encounter consistency issues.

Because the player doesn't exist, we need to:

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

However, should the client operation fail after the POST to /players, we've created a player that doesn't belong to a team:

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

Now we have an orphaned duplicate player in /players/5.

To fix this we might write custom recovery code that checks for orphaned players that match some natural key (e.g. Name). This is custom code that needs to be tested, costs more money and time etc etc

To avoid needing custom recovery code, we can implement PUT instead of POST.

From the RFC:

the intent of PUT is idempotent

For an operation to be idempotent, it needs to exclude external data such as server-generated id sequences. This is why people are recommending both PUT and UUIDs for Ids together.

This allows us to rerun both the /players PUT and the /memberships PUT without consequences:

PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
// *client failure*
// *client YOLOs*
PUT /players/23lkrjrqwlej { "Name": "Murray" } //=> 200 OK
PUT /teams/1/players/23lkrjrqwlej

Everything is fine and we didn't need to do anything more than retry for partial failures.

This is more of an addendum to the existing answers but I hope it puts them in context of the bigger picture of just how flexible and reliable ReST can be.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文