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

Posted

技术标签:

【中文标题】如何在 RESTful API 中处理多对多关系?【英文标题】:How can I handle many-to-many relationships in a RESTful API? 【发布时间】:2011-09-13 13:13:18 【问题描述】:

假设您有两个实体,PlayerTeam,玩家可以在多个团队中。在我的数据模型中,每个实体都有一个表,以及一个用于维护关系的连接表。 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 实体列表。要将玩家添加到团队,请使用适当构建的 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'
]

这方面的最佳做法是什么?

【问题讨论】:

你能更新接受的答案吗,还有另一个答案加倍投票,现在你可能有更好的经验来判断接受的答案的优缺点。 【参考方案1】:

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

    REST 是关于制作可进化的系统,如果没有别的。此时,您可能只关心给定球员是否在给定球队,但在未来的某个时候,您希望用更多数据来注释这种关系:他们已经待了多久在那个团队,谁把他们推荐给那个团队,他们的教练是谁/在那个团队时是谁,等等。 REST 依赖于缓存来提高效率,这需要考虑缓存原子性和失效。如果您向/teams/3/players/ 发布一个新实体,该列表将失效,但您不希望备用 URL /players/5/teams/ 保持缓存状态。是的,不同的缓存会有不同年龄的每个列表的副本,我们对此无能为力,但我们至少可以通过限制我们需要使无效的实体数量来最大限度地减少用户发布更新的混乱在他们客户的本地缓存中只有一个/memberships/98745(有关更详细的讨论,请参阅Helland 在Life beyond Distributed Transactions 中对“备用索引”的讨论)。 您可以通过简单地选择/players/5/teams/teams/3/players(但不能同时选择两者)来实现上述两点。让我们假设前者。然而,在某些时候,您可能希望将/players/5/teams/ 保留为当前 成员资格列表,但仍可以在某处引用过去 成员资格。将/players/5/memberships/ 设为/memberships/id/ 资源的超链接列表,然后您可以根据需要添加/players/5/past_memberships/,而无需打破每个人对单个会员资源的书签。这是一个笼统的概念;我相信您可以想象其他类似的未来更适用于您的具体情况。

【讨论】:

第 1 点和第 2 点已得到完美解释,谢谢,如果有人在现实生活中对第 3 点有更多了解,那将对我有所帮助。 最好和最简单的答案 IMO 谢谢!拥有两个端点并使它们保持同步会带来很多麻烦。 你好福满洲。问题:在其余端点 /memberships/98745 中,url 末尾的数字代表什么?它是会员的唯一ID吗?如何与会员端点交互?要添加玩家,是否会发送一个包含有效负载的 POST,其中包含 team : 3, player : 6 ,从而创建两者之间的链接?那么 GET 呢?您会向 /memberships?player= 和 /membersihps?team= 发送 GET 以获取结果吗?是这样的想法吗?我错过了什么吗? (我正在尝试学习 restful 端点)在这种情况下,memberships/98745 中的 id 98745 真的有用吗? @aruuuuu 应为关联提供单独的端点,并提供代理 PK。一般来说,它也让生活变得更轻松:/memberships/membershipId。密钥 (playerId, teamId) 保持唯一,因此可用于拥有此关系的资源:/teams/teamId/players 和 /players/playerId/teams。但这种关系并不总是在双方都保持的时候。例如,食谱和配料:您几乎不需要使用 /ingredients/ingredientId/recipes/。 如果您认为此处的“会员”等同于“TeamPlayers”,如上文***.com/a/51748211/511809 所述。这两个答案是相似的,互补的,都很棒。但是,对于复杂的 API,我建议不要为同一个想法使用太多名称(例如,为用户成员资格保留成员资格)。 api/teamplayers/ 完全没问题。【参考方案2】:

在 RESTful 接口中,您可以通过将这些关系编码为链接来返回描述资源之间关系的文档。因此,可以说一个团队有一个文档资源 (/team/id/players),它是团队中球员 (/player/id) 的链接列表,而一个球员可以有一个文档资源 (/player/id/teams),它是玩家所属球队的链接列表。漂亮而对称。您可以轻松地在该列表上进行地图操作,甚至为关系提供自己的 ID(可以说它们有两个 ID,具体取决于您是考虑团队优先还是玩家优先的关系),如果这会让事情变得更容易.唯一棘手的一点是,如果您从一端删除它,您必须记住从另一端删除关系,但是通过使用底层数据模型严格处理这个问题,然后让 REST 接口成为该模型将使这变得更容易。

关系 ID 可能应该基于 UUID 或同样长且随机的东西,而不管您为球队和球员使用的任何类型的 ID。这将让您在关系的每一端都使用与 ID 组件相同的 UUID,而不必担心冲突(小整数 有这个优势)。如果这些成员关系除了以双向方式将球员和球队联系起来之外还有其他任何属性,那么他们应该有自己的身份,独立于球员和球队;然后,玩家»团队视图 (/player/playerID/teams/teamID) 上的 GET 可以将 HTTP 重定向到双向视图 (/memberships/uuid)。

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

如果 team_player 有状态等附加信息怎么办?我们在您的模型中在哪里表示它?我们可以将其推广为资源,并为其提供URL,就像game/,player/一样 嘿,快速提问只是为了确保我做对了:GET /teams/1/players/3 返回一个空的响应正文。唯一有意义的响应是 200 与 404。 GET /teams/1/players/3 不会返回玩家实体的信息(姓名、年龄等)。如果客户端想要获得关于玩家的额外信息,他必须 GET /players/3。这一切都正确吗? 我同意您的映射,但有一个问题。这是个人意见的问题,但是您如何看待 POST /teams/1/players 以及您为什么不使用它?您认为这种方法有什么缺点/误导吗? POST 不是幂等的,即如果您执行 POST /teams/1/players n 次,您将更改 n 次 /teams/1。但是将玩家移动到 /teams/1 n 次不会改变团队的状态,因此使用 PUT 更明显。 @NarendraKamma 我想只是将status 作为 PUT 请求中的参数发送?这种方法有缺点吗?【参考方案4】:

我首选的解决方案是创建三个资源: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,因为返回的资源没有关于关系的信息,只有关于它自己的资源。

要处理 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返回TeamsPlayersteamId队伍中打过的所有TeamsPlayers。 在任一GET 调用中,都会返回资源TeamsPlayers。返回关系相关的所有数据。

当调用GET /Players/playerId/Teams(或GET /Teams/teamId/Players)时,资源Players(或Teams)调用TeamsPlayers使用查询过滤器返回相关球队(或球员)。

GET /Players/playerId/Teams 是这样工作的:

    查找 player 具有 id = playerId 的所有 TeamsPlayers。 (GET /TeamsPlayers?player=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 简单明了。

每当创建、更新或删除关系时,PlayersTeams 资源都会自动更新。

【讨论】:

引入 TeamsPlayers 资源真的很有意义。太棒了 最佳解释 这不是公认的,也不是大多数投票的答案。正如我所看到的,这种描述关系的新分离资源实际上解决了所有提到的问题。正确的 !?。现在,当我需要更多详细信息时,我会使用这个添加的新资源。除此之外,我会使用前面提到的简单资源一级嵌套。谢谢 非常实用的解释,我会为需要解决的类似情况选择此选项【参考方案5】:

现有的答案并没有解释一致性和幂等性的作用 - 这促使他们建议使用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 序列。这就是为什么人们将PUTUUIDs 一起推荐给Ids。

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

【讨论】:

在这个假设的端点中,您从哪里获得 23lkrjrqwlej 键盘上的滚动面 - 23lkr 没有什么特别的... gobbledegook 除了它不是连续的或有意义的 如果客户端在重试之前退出了怎么办?如果一个玩家没有团队就不能存在,这不应该是服务器上的交易吗? @Eladian 我认为您正在考虑锁定分布式事务。如果您在没有锁定的情况下执行此操作,那么事务的任何故障和交付容错编排基本上都归结为验证和重试,客户端负责可靠地保持不完整的系统状态。人们这样做的一种方法是将消息处理到一些可靠的、持久的队列或事件流中;如果接收器按照我上面提到的方式行事,这真的很简单。这是一个绝对庞大的问题,你已经问过很多关于它的书了:) 很好的答案,但您的所有第一个 POST /players "Name": "Murray" 都应该返回 201(已创建)和 Location 标头而不是 302。【参考方案6】:

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

假设 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"
        ,
        ...
    ]

【讨论】:

【参考方案7】:
    /players(是主资源) /teams/id/players(是关系资源,所以它的反应与1不同) /memberships(是一种关系,但语义复杂) /players/memberships(是一种关系,但语义复杂)

我更喜欢 2

【讨论】:

也许我只是没看懂答案,但这篇文章似乎没有回答问题。 这没有提供问题的答案。要批评或要求作者澄清,请在他们的帖子下方发表评论 - 您可以随时评论自己的帖子,一旦您有足够的reputation,您就可以comment on any post。 @IllegalArgument 它一个答案,作为评论没有意义。然而,这不是最好的答案。 这个答案很难理解,也没有提供理由。 这根本不能解释或回答所提出的问题。

以上是关于如何在 RESTful API 中处理多对多关系?的主要内容,如果未能解决你的问题,请参考以下文章

多对多资源映射restful api设计

如何使用 Fluent Api 命名多对多关系表?

如何在 django 中处理未保存的多对多关系?

EF Core 5,删除多对多关系

在 CloudKit 中处理多对多关系

如何通过 Fluent API Entity Framework 定义多对多关系?