Como lidar com relacionamentos muitos para muitos em uma API RESTful?


288

Imagine que você tem 2 entidades, Jogador e Equipe , onde os jogadores podem estar em várias equipes. No meu modelo de dados, tenho uma tabela para cada entidade e uma tabela de junção para manter os relacionamentos. O Hibernate é bom em lidar com isso, mas como posso expor esse relacionamento em uma API RESTful?

Eu posso pensar em algumas maneiras. Primeiro, posso fazer com que cada entidade contenha uma lista da outra, para que um objeto Player tenha uma lista de equipes às quais pertence e cada objeto Team tenha uma lista de jogadores que pertencem a ela. Então, para adicionar um jogador a uma equipe, você apenas colocaria a representação do jogador em um endpoint, algo como POST /playerou POST /teamcom o objeto apropriado como a carga útil da solicitação. Isso parece o mais "RESTful" para mim, mas parece um pouco estranho.

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

A outra maneira que posso pensar em fazer isso seria expor o relacionamento como um recurso por si só. Portanto, para ver uma lista de todos os jogadores de um determinado time, você pode fazer um GET /playerteam/team/{id}ou algo assim e recuperar uma lista de entidades do PlayerTeam. Para adicionar um jogador a uma equipe, POST /playerteamcom uma entidade PlayerTeam criada adequadamente como a carga útil.

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

Qual é a melhor prática para isso?

Respostas:


129

Em uma interface RESTful, você pode retornar documentos que descrevem os relacionamentos entre recursos, codificando esses relacionamentos como links. Assim, pode-se dizer que um time possui um recurso de documento ( /team/{id}/players) que é uma lista de links para jogadores ( /player/{id}) do time, e um jogador pode ter um recurso de documento ()/player/{id}/teams) que é uma lista de links para equipes das quais o jogador é membro. Bom e simétrico. Você pode mapear as operações nessa lista com bastante facilidade, mesmo fornecendo aos seus próprios IDs um relacionamento (sem dúvida eles teriam dois IDs, dependendo se você está pensando no relacionamento equipe primeiro ou primeiro jogador), se isso facilitar as coisas . A única parte complicada é que você deve se lembrar de excluir o relacionamento da outra extremidade também, se você o excluir de uma extremidade, mas manipulando-o rigorosamente usando um modelo de dados subjacente e fazendo com que a interface REST seja uma visão do esse modelo vai facilitar isso.

Os IDs de relacionamento provavelmente devem ser baseados em UUIDs ou algo igualmente longo e aleatório, independentemente de qualquer tipo de ID que você use para equipes e jogadores. Isso permitirá que você use o mesmo UUID que o componente de ID para cada extremidade do relacionamento sem se preocupar com colisões (números inteiros pequenos não têm essa vantagem). Se esses relacionamentos de associação tiverem outras propriedades além do simples fato de relacionarem um jogador e uma equipe de maneira bidirecional, eles deverão ter uma identidade própria, independente de jogadores e equipes; um GET na visualização player »team ( /player/{playerID}/teams/{teamID}) poderia fazer um redirecionamento HTTP para a visualização bidirecional ( /memberships/{uuid}).

Eu recomendo escrever links em qualquer documento XML que você retornar (se estiver produzindo XML, é claro) usando atributos XLink xlink:href .


265

Faça um conjunto separado de /memberships/recursos.

  1. O REST é sobre a criação de sistemas evolutivos, se nada mais. Neste momento, você só pode importar-se que um determinado jogador está em uma determinada equipe, mas em algum momento no futuro, você vai querer anotar que o relacionamento com mais dados: quanto tempo eles estiveram naquele time, que os referidos para aquele time, quem é o treinador dele / estava enquanto estava naquele time, etc.
  2. O REST depende do armazenamento em cache para obter eficiência, o que requer alguma consideração quanto à atomicidade e invalidação do cache. Se você POSTAR uma nova entidade para /teams/3/players/essa lista, será invalidada, mas não deseja que o URL alternativo /players/5/teams/permaneça em cache. Sim, caches diferentes terão cópias de cada lista com idades diferentes, e não há muito o que fazer sobre isso, mas podemos pelo menos minimizar a confusão para o usuário postar a atualização, limitando o número de entidades que precisamos invalidar no cache local do cliente para um e apenas um em /memberships/98745(consulte a discussão de Helland sobre "índices alternativos" em Vida além de transações distribuídas para uma discussão mais detalhada).
  3. Você pode implementar os 2 pontos acima simplesmente escolhendo /players/5/teamsou /teams/3/players(mas não ambos). Vamos assumir o primeiro. Em algum momento, no entanto, você deverá reservar /players/5/teams/uma lista de associações atuais e ainda poderá se referir a associações anteriores em algum lugar. Faça /players/5/memberships/uma lista de hiperlinks para /memberships/{id}/recursos e, em seguida, você pode adicionar /players/5/past_memberships/quando quiser, sem precisar quebrar os indicadores de todos os recursos de associação individuais. Este é um conceito geral; Tenho certeza de que você pode imaginar outros futuros semelhantes que são mais aplicáveis ​​ao seu caso específico.

11
Os pontos 1 e 2 são perfeitamente explicados, obrigado, se alguém tiver mais carne para o ponto 3 na experiência da vida real, isso me ajudaria.
Alain

2
Melhor e mais simples resposta obrigado IMO! Ter dois pontos finais e mantê-los sincronizados tem uma série de complicações.
Venkat D.

7
oi fumanchu. Perguntas: No ponto de extremidade restante / associações / 98745, o que esse número no final do URL representa? É um ID exclusivo para a associação? Como alguém interage com o endpoint de associação? Para adicionar um jogador, um POST seria enviado contendo uma carga útil com {team: 3, player: 6}, criando assim o link entre os dois? Que tal um GET? você enviaria um GET para / members? player = e / membersihps? team = para obter resultados? Essa é a ideia? Estou faltando alguma coisa? (Estou tentando aprender pontos de extremidade tranqüilos) Nesse caso, o ID 98745 em associações / 98745 é realmente útil?
Aruuuuu 6/01/17

@aruuuuu, um ponto de extremidade separado para uma associação deve receber um PK substituto. Torna a vida muito mais fácil também em geral: / memberships / {MembershipId}. A chave (playerId, teamId) permanece única e, portanto, pode ser usada nos recursos que possuem essa relação: / teams / {teamId} / players e / players / {playerId} / teams. Mas nem sempre é quando essas relações são mantidas nos dois lados. Por exemplo, Receitas e ingredientes: você quase nunca precisará usar / ingredientes / {ingredienteId} / receitas /.
Alexander Palamarchuk

65

Gostaria de mapear essa relação com sub-recursos, o design geral / travessia seria:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

Em termos repousantes, ajuda muito a não pensar em SQL e une-se, mas mais em coleções, sub-coleções e travessias.

Alguns exemplos:

# 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

Como você vê, não uso o POST para colocar jogadores nas equipes, mas o PUT, que lida melhor com o seu relacionamento n: n de jogadores e equipes.


20
E se o team_player tiver informações adicionais, como status etc.? onde o representamos no seu modelo? podemos promovê-lo a um recurso, e fornecer URLs para ele, assim como jogo /, / leitor
Narendra Kamma

Ei, pergunta rápida, apenas para ter certeza de que estou acertando: GET / teams / 1 / players / 3 retorna um corpo de resposta vazio. A única resposta significativa disso é 200 vs 404. As informações da entidade do jogador (nome, idade, etc.) NÃO são retornadas por GET / teams / 1 / players / 3. Se o cliente quiser obter informações adicionais sobre o jogador, ele deve obter GET / players / 3. Está tudo correto?
Verdagon

2
Concordo com o seu mapeamento, mas tenho uma pergunta. É questão de opinião pessoal, mas o que você acha do POST / equipes / 1 / jogadores e por que você não o usa? Você vê alguma desvantagem / enganosa nessa abordagem?
JakubKnejzlik

2
O POST não é idempotente, ou seja, se você fizer o POST / times / 1 / jogadores n-times, você mudaria n-times / times / 1. mas mover um jogador para / teams / 1 n-times não mudará o estado do time, então usar PUT é mais óbvio.
manuel aldana

1
@NarendraKamma Presumo que apenas envie statuscomo param na solicitação PUT? Existe uma desvantagem nessa abordagem?
Traxo 4/18

22

As respostas existentes não explicam os papéis de consistência e idempotência - que motivam suas recomendações de UUIDs/ números aleatórios para IDs e PUTnão POST.

Se considerarmos o caso em que temos um cenário simples como " Adicionar um novo jogador a um time ", encontraremos problemas de consistência.

Como o player não existe, precisamos:

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

No entanto, se a operação do cliente falhar após o POSTque /players, criamos um jogador que não pertence a uma equipe:

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

Agora temos um reprodutor duplicado órfão /players/5.

Para corrigir isso, podemos escrever um código de recuperação personalizado que verifique se há jogadores órfãos que correspondem a alguma chave natural (por exemplo Name). Este é um código personalizado que precisa ser testado, custa mais dinheiro e tempo, etc.

Para evitar a necessidade de código de recuperação personalizado, podemos implementar em PUTvez de POST.

Do RFC :

a intenção de PUTé idempotente

Para que uma operação seja idempotente, ela precisa excluir dados externos, como sequências de identificação geradas pelo servidor. É por isso que as pessoas recomendam es PUTe es juntos.UUIDId

Isso nos permite executar novamente /players PUTas /memberships PUTconsequências sem e com:

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

Está tudo bem e não precisamos fazer nada além de tentar novamente por falhas parciais.

Este é mais um adendo às respostas existentes, mas espero que as coloque no contexto de uma imagem maior de quão flexível e confiável o ReST pode ser.


Nesse ponto de extremidade hipotético, de onde você tirou isso 23lkrjrqwlej?
cbcoutinho 14/01

1
rolar a cara no teclado - não há nada de especial nos 23lkr ... gobbledegook que não seja sequencial ou significativo
Seth

9

Minha solução preferida é a criação de três recursos: Players, Teamse TeamsPlayers.

Portanto, para obter todos os jogadores de uma equipe, basta acessar os Teamsrecursos e obter todos os jogadores ligando GET /Teams/{teamId}/Players.

Por outro lado, para obter todos os times que um jogador jogou, obtenha o Teamsrecurso dentro do Players. Ligue GET /Players/{playerId}/Teams.

E, para receber a ligação de relacionamento muitos-para-muitos GET /Players/{playerId}/TeamsPlayersou GET /Teams/{teamId}/TeamsPlayers.

Observe que, nesta solução, quando você liga GET /Players/{playerId}/Teams, você obtém uma matriz de Teamsrecursos, que é exatamente o mesmo recurso que você recebe quando liga GET /Teams/{teamId}. O inverso segue o mesmo princípio, você obtém uma matriz de Playersrecursos quando chamaGET /Teams/{teamId}/Players .

Nas duas chamadas, nenhuma informação sobre o relacionamento é retornada. Por exemplo, no contractStartDateé retornado, porque o recurso retornado não possui informações sobre o relacionamento, apenas sobre seu próprio recurso.

Para lidar com o relacionamento nn, ligue para GET /Players/{playerId}/TeamsPlayersou GET /Teams/{teamId}/TeamsPlayers. Essas chamadas retornam exatamente o recurso TeamsPlayers,.

Este TeamsPlayersrecurso tem id, playerId,teamId atributos, bem como alguns outros para descrever a relação. Além disso, possui os métodos necessários para lidar com eles. GET, POST, PUT, DELETE etc que retornarão, incluirão, atualizarão, removerão o recurso de relacionamento.

O TeamsPlayersrecurso implementa algumas consultas, como GET /TeamsPlayers?player={playerId}retornar todos os TeamsPlayersrelacionamentos identificados pelo jogador {playerId}. Seguindo a mesma idéia, use GET /TeamsPlayers?team={teamId}para retornar tudo o TeamsPlayersque jogou no {teamId}time. Em qualquer GETchamada, o recurso TeamsPlayersé retornado. Todos os dados relacionados ao relacionamento são retornados.

Ao chamar GET /Players/{playerId}/Teams(ou GET /Teams/{teamId}/Players), o recurso Players(ou Teams) chama TeamsPlayerspara retornar as equipes (ou jogadores) relacionadas usando um filtro de consulta.

GET /Players/{playerId}/Teams funciona assim:

  1. Encontre todos os jogadores do TeamsPlayers que o jogador possua id = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Repetir os TeamsPlayers retornados
  3. Usando o teamId obtido do TeamsPlayers , chame GET /Teams/{teamId}e armazene os dados retornados
  4. Depois que o loop terminar. Retorne todas as equipes que foram informadas.

Você pode usar o mesmo algoritmo para obter todos os jogadores de uma equipe ao fazer uma chamada GET /Teams/{teamId}/Players, mas trocar equipes e jogadores.

Meus recursos ficariam assim:

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

Esta solução depende apenas dos recursos REST. Embora algumas chamadas extras possam ser necessárias para obter dados de jogadores, equipes ou seu relacionamento, todos os métodos HTTP são facilmente implementados. POST, PUT, DELETE são simples e diretos.

Sempre que um relacionamento é criado, atualizado ou excluído, tanto Playerse Teamsrecursos são atualizados automaticamente.


ele realmente faz sentido introduzir TeamsPlayers resource.Awesome
Vijay

melhor explicação
Diana

1

Sei que há uma resposta marcada como aceita para esta pergunta, no entanto, eis como podemos resolver os problemas levantados anteriormente:

Digamos que PUT

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

Como exemplo, todos os itens a seguir resultarão no mesmo efeito sem a necessidade de sincronização, porque são feitos em um único recurso:

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

agora, se quisermos atualizar várias associações para uma equipe, poderíamos fazer o seguinte (com validações apropriadas):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}

-3
  1. / players (é um recurso mestre)
  2. / teams / {id} / players (é um recurso de relacionamento, portanto reage de maneira diferente de 1)
  3. / associações (é um relacionamento, mas semanticamente complicado)
  4. / players / associações (é um relacionamento, mas semanticamente complicado)

Eu prefiro 2


2
Talvez eu simplesmente não entenda a resposta, mas este post não parece responder à pergunta.
21314 BradleyDotNET

Isso não fornece uma resposta para a pergunta. Para criticar ou solicitar esclarecimentos a um autor, deixe um comentário abaixo da postagem - você sempre pode comentar em suas próprias postagens e, quando tiver reputação suficiente , poderá comentar em qualquer post .
Argumento ilegal

4
@IllegalArgument Ele é uma resposta e não faria sentido como um comentário. No entanto, não é a melhor resposta.
Qix - MONICA FOI ERRADA EM

1
Esta resposta é difícil de seguir e não fornece razões.
Venkat D. 08/01

2
Isso não explica nem responde à pergunta feita.
Manjit Kumar 01/02
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.