首页 文章

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

提问于
浏览
236

想象一下,你有2个实体, PlayerTeam ,玩家可以在多个团队中 . 在我的数据模型中,我有一个每个实体的表,以及一个用于维护关系的连接表 . Hibernate可以很好地处理这个问题,但是我如何在RESTful API中公开这种关系呢?

我可以想几个方面 . 首先,我可能让每个实体都包含另一个实体的列表,因此Player对象将拥有它所属的Teams列表,并且每个Team对象都有一个属于它的Players列表 . 因此,要向团队添加播放器,您只需将播放器的表示形式发布到 endpoints ,例如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实体列表 . 要将球员添加到球队,请使用适当构建的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'        
]

这是什么最好的做法?

7 回答

  • 50

    在RESTful接口中,您可以通过将这些关系编码为链接来返回描述资源之间关系的文档 . 因此,可以说一个团队有一个文档资源( /team/{id}/players ),它是团队中玩家( /player/{id} )的链接列表,玩家可以拥有一个文档资源( /player/{id}/teams ),该资源是指向团队的链接列表 . 玩家是其中的一员 . 不错,对称 . 你可以很容易地在该列表上进行 Map 操作,甚至可以给自己的关系提供一个关系(可以说他们正在思考关系团队优先或玩家优先),如果这样可以让事情变得更容易 . 唯一棘手的一点是你必须记住从另一端删除关系,如果你从一端删除它,但是通过使用底层数据模型严格处理它,然后让REST接口成为一个视图该模型将使这更容易 .

    关系ID可能应该基于UUID或同样长且随机的东西,无论您为团队和玩家使用何种类型的ID . 这将允许您为关系的每一端使用相同的UUID作为ID组件,而不必担心冲突(小整数没有这种优势) . 如果这些成员关系具有除了与玩家和团队双向关联的事实以外的任何属性,他们应该拥有独立于玩家和团队的自己的身份;播放器»团队视图( /player/{playerID}/teams/{teamID} )上的GET然后可以进行HTTP重定向到双向视图( /memberships/{uuid} ) .

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

  • 225

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

    • REST是关于制作可进化的系统,如果没有别的 . 此时,您可能只关心给定球员在给定球队中,但在将来的某个时刻,您会想要用更多数据来注释这种关系:他们在该球队上呆了多长时间,谁推荐他们对那支球队来说,他们的教练是谁/同时在那支球队等等 .

    • REST依赖于缓存效率,这需要考虑缓存原子性和失效 . 如果您将新实体POST到 /teams/3/players/ ,该列表将失效,但您不希望备用URL /players/5/teams/ 保持缓存状态 . 是的,不同的缓存将包含具有不同年龄的每个列表的副本,并且通过将我们需要在其客户端的本地缓存中无效的实体数量限制为一个且仅在 /memberships/98745 处有一个来实现更新(参见Helland在Life beyond Distributed Transactions中对"alternate indices"的讨论)有关更详细的讨论) .

    • 您只需选择 /players/5/teams/teams/3/players (但不能同时选择两者)即可实现上述2点 . 让我们假设前者 . 但是,在某些时候,您需要保留 /players/5/teams/ 以获取当前成员资格的列表,并且能够在某处引用过去的成员资格 . 使 /players/5/memberships/ 成为 /memberships/{id}/ 资源的超链接列表,然后您可以在需要时添加 /players/5/past_memberships/ ,而不必打破所有人's bookmarks for the individual membership resources. This is a general concept; I'确定您可以想象其他类似的期货更适用于您的特定情况 .

  • 104

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

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

    在Restful-terms中,它不仅没有考虑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关系 .

  • 12

    现有的答案并不能解释其作用一致性和幂等性 - 它激发了他们的建议 UUIDs / ID的随机数和 PUT 而不是 POST .

    如果我们考虑的情况是我们有一个简单的场景,如“向团队添加新玩家”,我们会遇到一致性问题 .

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

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

    但是,如果客户端操作在 POST/players 之后失败,我们've created a player that doesn't属于一个团队:

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

    现在我们在 /players/5 中有一个孤立的重复播放器 .

    要解决此问题,我们可能会编写自定义恢复代码,以检查与某些自然密钥匹配的孤立播放器(例如 Name ) . 这是需要测试的自定义代码,需要花费更多的金钱和时间等

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

    来自RFC

    PUT的意图是幂等的

    对于幂等的操作,它需要排除外部数据,例如服务器生成的id序列 . 这就是人们为 Id 一起推荐 PUTUUID 的原因 .

    这允许我们重新运行 /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可以灵活可靠的大局观背景下 .

  • -3

    我首选的解决方案是创建三个资源: 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} 返回在 {teamId} 团队中玩过的所有 TeamsPlayers . 在 GET 调用中,返回资源 TeamsPlayers . 返回与关系相关的所有数据 .

    当调用 GET /Players/{playerId}/Teams (或 GET /Teams/{teamId}/Players )时,资源 Players (或 Teams )调用 TeamsPlayers 以使用查询过滤器返回相关团队(或玩家) .

    GET /Players/{playerId}/Teams 的工作原理如下:

    找到玩家拥有id = playerId的所有TeamsPlayers . (GET / TeamsPlayers?player = )循环返回的TeamsPlayers使用从TeamsPlayers获得的teamId,调用GET / Teams / 并在循环结束后存储返回的数据 . 返回循环中获得的所有团队 .

    在拨打 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 资源 .

  • 3

    我知道这个问题的答案被标记为已接受,但是,这里是我们如何解决之前提出的问题:

    让我们说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"
            },
            ...
        ]
    }
    
  • 1
    • / players(是主要资源)

    • / teams / / players(是一种关系资源,所以它的反应不同1)

    • / membership(是一种关系,但在语义上很复杂)

    • /球员/会员(是一种关系,但在语义上很复杂)

    我更喜欢2

相关问题