Come gestire le relazioni molti-a-molti in un'API RESTful?


288

Immagina di avere 2 entità, Giocatore e Squadra , in cui i giocatori possono essere in più squadre. Nel mio modello di dati, ho una tabella per ogni entità e una tabella di join per mantenere le relazioni. Hibernate è bravo a gestirlo, ma come potrei esporre questa relazione in un'API RESTful?

Mi viene in mente un paio di modi. In primo luogo, potrei avere ciascuna entità contenente un elenco dell'altra, quindi un oggetto Giocatore avrebbe un elenco di Squadre a cui appartiene e ogni oggetto Squadra avrebbe un elenco di Giocatori che gli appartengono. Quindi per aggiungere un giocatore a una squadra, devi semplicemente POSTARE la rappresentazione del giocatore a un endpoint, qualcosa come POST /playero POST /teamcon l'oggetto appropriato come payload della richiesta. Questo mi sembra il più "RESTOSO" ma sembra un po 'strano.

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

L'altro modo in cui mi viene in mente di fare questo sarebbe quello di esporre la relazione come una risorsa a sé stante. Quindi, per vedere un elenco di tutti i giocatori di una determinata squadra, potresti fare un GET /playerteam/team/{id}o qualcosa del genere e recuperare un elenco di entità PlayerTeam. Per aggiungere un giocatore a una squadra, POST /playerteamcon un'entità PlayerTeam adeguatamente costruita come 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'        
]

Qual è la migliore pratica per questo?

Risposte:


129

In un'interfaccia RESTful, è possibile restituire documenti che descrivono le relazioni tra risorse codificando tali relazioni come collegamenti. Pertanto, si può dire che una squadra ha una risorsa documento ( /team/{id}/players) che è un elenco di collegamenti ai giocatori ( /player/{id}) nella squadra e che un giocatore può avere una risorsa documento (/player/{id}/teams) che è un elenco di collegamenti a squadre di cui il giocatore è membro. Bello e simmetrico. Puoi eseguire le operazioni sulla mappa in quell'elenco abbastanza facilmente, anche dando a una relazione i propri ID (probabilmente avrebbero due ID, a seconda che tu stia pensando al rapporto squadra prima o giocatore prima) se ciò semplifica le cose . L'unico inconveniente è che devi ricordare di eliminare anche la relazione dall'altra parte se la elimini da un'estremità, ma gestendola rigorosamente utilizzando un modello di dati sottostante e avendo quindi l'interfaccia REST una vista di quel modello renderà tutto più semplice.

Gli ID di relazione dovrebbero probabilmente essere basati su UUID o qualcosa di altrettanto lungo e casuale, indipendentemente dal tipo di ID che usi per squadre e giocatori. Ciò ti consentirà di utilizzare lo stesso UUID del componente ID per ciascuna estremità della relazione senza preoccuparti delle collisioni (i numeri interi piccoli non hanno questo vantaggio). Se queste relazioni di appartenenza hanno proprietà diverse dal semplice fatto che mettono in relazione un giocatore e una squadra in modo bidirezionale, dovrebbero avere una propria identità indipendente da giocatori e squadre; un OTTENERE sulla vista squadra »giocatore ( /player/{playerID}/teams/{teamID}) potrebbe quindi eseguire un reindirizzamento HTTP alla vista bidirezionale ( /memberships/{uuid}).

Ti consiglio di scrivere collegamenti in qualsiasi documento XML che restituisci (se ti capita di produrre XML ovviamente) usando gli attributi XLink xlink:href .


265

Crea un insieme separato di /memberships/risorse.

  1. REST consiste nel realizzare sistemi evolutivi se non altro. In questo momento, si può interessa soltanto che un dato giocatore è su un dato squadra, ma ad un certo punto, in futuro, si avrà voglia di annotare quel rapporto con più dati: per quanto tempo sono stati in quella squadra, che li riferisce a quella squadra, chi è / era il suo allenatore in quella squadra, ecc. ecc.
  2. Il REST dipende dalla memorizzazione nella cache per l'efficienza, che richiede una certa considerazione per l'atomicità e l'invalidazione della cache. Se POST una nuova entità in /teams/3/players/tale elenco verrà invalidata, ma non si desidera che l'URL alternativo /players/5/teams/rimanga memorizzato nella cache. Sì, cache diverse avranno copie di ogni elenco con età diverse, e non c'è molto che possiamo fare al riguardo, ma possiamo almeno minimizzare la confusione per l'utente che POST'aggiornamento aggiornando il numero di entità che dobbiamo invalidare nella cache locale dei loro clienti a uno e uno solo a /memberships/98745(vedi la discussione di Helland sugli "indici alternativi" in La vita oltre le transazioni distribuite per una discussione più dettagliata).
  3. È possibile implementare i 2 punti precedenti semplicemente selezionando /players/5/teamso /teams/3/players(ma non entrambi). Supponiamo che il primo. Ad un certo punto, tuttavia, ti consigliamo di prenotare /players/5/teams/un elenco di abbonamenti attuali e di poter comunque fare riferimento agli abbonamenti passati da qualche parte. Crea /players/5/memberships/un elenco di collegamenti ipertestuali alle /memberships/{id}/risorse, quindi puoi aggiungerlo /players/5/past_memberships/quando vuoi, senza dover interrompere i segnalibri di tutti per le singole risorse di appartenenza. Questo è un concetto generale; Sono sicuro che puoi immaginare altri futuri simili che sono più applicabili al tuo caso specifico.

11
I punti 1 e 2 sono spiegati perfettamente, grazie, se qualcuno ha più carne per il punto 3 nell'esperienza di vita reale, questo mi aiuterebbe.
Alain,

2
Risposta migliore e più semplice IMO grazie! Avere due endpoint e mantenerli sincronizzati presenta una serie di complicazioni.
Venkat D.

7
ciao fumanchu. Domande: nel resto endpoint / memberships / 98745 cosa rappresenta quel numero alla fine dell'URL? È un ID unico per l'iscrizione? Come interagirebbe con l'endpoint delle appartenenze? Per aggiungere un giocatore verrebbe inviato un POST contenente un payload con {team: 3, player: 6}, creando così il collegamento tra i due? Che ne dici di OTTENERE? manderesti un OTTENERE a / membership? player = e / membersihps? team = per ottenere risultati? Questa è l'idea? Mi sto perdendo qualcosa? (Sto cercando di apprendere endpoint riposanti) In tal caso, l'id 98745 in membership / 98745 è mai davvero utile?
aruuuuu

@aruuuuu deve essere fornito un endpoint separato per un'associazione con un PK surrogato. Rende la vita molto più semplice anche in generale: / memberships / {membershipId}. La chiave (playerId, teamId) rimane unica e quindi può essere utilizzata sulle risorse che possiedono questa relazione: / teams / {teamId} / players e / players / {playerId} / teams. Ma non è sempre quando tali relazioni sono mantenute da entrambe le parti. Ad esempio, ricette e ingredienti: difficilmente avrai mai bisogno di usare / ingredienti / {ingredienteId} / ricette /.
Alexander Palamarchuk,

65

Vorrei mappare tale relazione con le risorse secondarie, il disegno / attraversamento generale sarebbe quindi:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

In termini riposanti aiuta molto a non pensare a SQL e ai join, ma piuttosto a raccolte, sotto-raccolte e attraversamenti.

Qualche esempio:

# 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

Come vedi, non uso POST per posizionare i giocatori nelle squadre ma PUT, che gestisce meglio la tua relazione n: n tra giocatori e squadre.


20
Cosa succede se team_player ha informazioni aggiuntive come lo stato, ecc.? dove lo rappresentiamo nel tuo modello? possiamo promuoverlo a una risorsa e fornirgli URL, proprio come gioco /, giocatore /
Narendra Kamma

Ehi, domanda veloce solo per essere sicuro che sto andando bene: GET / teams / 1 / players / 3 restituisce un corpo di risposta vuoto. L'unica risposta significativa da questo è 200 vs 404. Le informazioni dell'entità giocatore (nome, età, ecc.) NON vengono restituite da GET / teams / 1 / players / 3. Se il cliente desidera ottenere ulteriori informazioni sul giocatore, deve OTTENERE / players / 3. È tutto corretto?
Verdagon,

2
Sono d'accordo con la tua mappatura, ma ho una domanda. È una questione di opinione personale, ma cosa ne pensi di POST / team / 1 / giocatori e perché non lo usi? Vedi qualche svantaggio / fuorviante in questo approccio?
JakubKnejzlik,

2
Il POST non è idempotente, ovvero se si esegue POST / squadre / 1 / giocatori n-volte, si cambierebbe n-volte / squadre / 1. ma spostare un giocatore in / squadre / 1 volte non cambierà lo stato della squadra, quindi usare PUT è più ovvio.
manuel aldana,

1
@NarendraKamma Presumo solo di inviare statuscome parametro nella richiesta PUT? C'è un aspetto negativo di questo approccio?
Traxo,

22

Le risposte esistenti non spiegano i ruoli di coerenza e idempotenza - che motivano le loro raccomandazioni di UUIDs/ numeri casuali per ID e PUTinvece di POST.

Se consideriamo il caso in cui abbiamo uno scenario semplice come " Aggiungi un nuovo giocatore a una squadra ", riscontriamo problemi di coerenza.

Poiché il giocatore non esiste, dobbiamo:

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

Tuttavia, se l'operazione client fallisce dopo il POSTto /players, abbiamo creato un giocatore che non appartiene a una squadra:

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

Ora abbiamo un duplicato orfano dentro /players/5 .

Per risolvere questo problema, potremmo scrivere un codice di recupero personalizzato che controlla i giocatori orfani che corrispondono a una chiave naturale (ad es Name ). Questo è un codice personalizzato che deve essere testato, costa più tempo e denaro, ecc. Ecc

Per evitare la necessità di un codice di ripristino personalizzato, possiamo implementare PUTanzichéPOST .

Dalla RFC :

l'intento di PUTè idempotente

Perché un'operazione sia idempotente, deve escludere dati esterni come sequenze di ID generate dal server. Questo è il motivo per cui le persone raccomandano entrambi PUTe UUIDsId s insieme.

Questo ci consente di rieseguire sia il /players PUTche il /memberships PUTsenza conseguenze:

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

Va tutto bene e non abbiamo dovuto fare altro che riprovare per guasti parziali.

Questo è più un addendum alle risposte esistenti, ma spero che le inserisca nel quadro più ampio di quanto ReST possa essere flessibile e affidabile.


In questo ipotetico endpoint, da dove hai preso 23lkrjrqwlej?
cbcoutinho,

1
ruota la faccia sulla tastiera - non c'è niente di speciale nel 23lkr ... gobbledegook diverso da quello che non è sequenziale o significativo
Seth

9

La mia soluzione preferita è quella di creare tre risorse: Players, TeamseTeamsPlayers .

Quindi, per ottenere tutti i giocatori di una squadra, basta andare alle Teamsrisorse e ottenere tutti i suoi giocatori chiamandoGET /Teams/{teamId}/Players .

D'altra parte, per ottenere tutte le squadre che un giocatore ha giocato, ottenere la Teamsrisorsa all'interno di Players. ChiamataGET /Players/{playerId}/Teams .

E, per ottenere la chiamata di relazione molti-a-molti GET /Players/{playerId}/TeamsPlayersoGET /Teams/{teamId}/TeamsPlayers .

Si noti che, in questa soluzione, quando si chiama GET /Players/{playerId}/Teams, si ottiene una matrice di Teamsrisorse, che è esattamente la stessa risorsa che si ottiene quando si chiama GET /Teams/{teamId}. Il contrario segue lo stesso principio, Playersquando si chiama si ottiene una serie di risorseGET /Teams/{teamId}/Players .

In entrambe le chiamate, non viene restituita alcuna informazione sulla relazione. Ad esempio nocontractStartDate viene restituito, poiché la risorsa restituita non ha informazioni sulla relazione, ma solo sulla propria risorsa.

Per gestire la relazione nn, chiama GET /Players/{playerId}/TeamsPlayerso GET /Teams/{teamId}/TeamsPlayers. Queste chiamate restituiscono esattamente la risorsa,TeamsPlayers .

Questa TeamsPlayersrisorsa ha id, playerId,teamId attributi, così come alcuni altri per descrivere la relazione. Inoltre, ha i metodi necessari per gestirli. OTTIENI, POST, PUT, ELIMINA ecc. Che restituiranno, includeranno, aggiorneranno, rimuoveranno la risorsa di relazione.

La TeamsPlayersrisorsa implementa alcune query, come GET /TeamsPlayers?player={playerId}restituire tutte le TeamsPlayersrelazioni che il giocatore identificato {playerId}ha. Seguendo la stessa idea, usa GET /TeamsPlayers?team={teamId}per restituire tutto TeamsPlayersciò che ha giocato nella {teamId}squadra. In entrambe le GETchiamate, la risorsa TeamsPlayersviene restituita. Vengono restituiti tutti i dati relativi alla relazione.

Quando si chiama GET /Players/{playerId}/Teams(o GET /Teams/{teamId}/Players), la risorsa Players(o Teams) chiamaTeamsPlayers per restituire le squadre (o i giocatori) correlate utilizzando un filtro di query.

GET /Players/{playerId}/Teams funziona così:

  1. Trova tutti i TeamsPlayer che il giocatore ha id = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Ripeti in loop i giocatori di Teams restituiti
  3. Utilizzando il teamId ottenuto da TeamsPlayer , chiama GET /Teams/{teamId}e archivia i dati restituiti
  4. Al termine del ciclo. Restituisci tutte le squadre che sono state messe in loop.

È possibile utilizzare lo stesso algoritmo per ottenere tutti i giocatori da una squadra, quando si chiama GET /Teams/{teamId}/Players, ma scambiando squadre e giocatori.

Le mie risorse sarebbero così:

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

Questa soluzione si basa solo sulle risorse REST. Sebbene possano essere necessarie alcune chiamate extra per ottenere dati da giocatori, squadre o loro relazione, tutti i metodi HTTP sono facilmente implementabili. POST, PUT, DELETE sono semplici e diretti.

Ogni volta che viene creata, aggiornata o eliminata una relazione, entrambi Playerse le Teamsrisorse vengono automaticamente aggiornate.


ha davvero senso introdurre la risorsa di
TeamsPlayer

miglior spiegazione
Diana,

1

So che esiste una risposta contrassegnata come accettata per questa domanda, tuttavia, ecco come possiamo risolvere i problemi precedentemente sollevati:

Diciamo per PUT

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

Ad esempio, i seguenti risultati si tradurranno tutti nello stesso effetto senza necessità di sincronizzazione poiché vengono eseguiti su una singola risorsa:

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

ora, se vogliamo aggiornare più abbonamenti per un team, potremmo fare come segue (con convalide appropriate):

PUT    /membership/teams/team1/

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

-3
  1. / players (è una risorsa principale)
  2. / teams / {id} / players (è una risorsa di relazione, quindi reagisce in modo diverso da 1)
  3. / memberships (è una relazione ma semanticamente complicata)
  4. / players / memberships (è una relazione ma semanticamente complicata)

Preferisco 2


2
Forse non capisco la risposta, ma questo post non sembra rispondere alla domanda.
BradleyDotNET,

Questo non fornisce una risposta alla domanda. Per criticare o richiedere chiarimenti a un autore, lascia un commento sotto il suo post: puoi sempre commentare i tuoi post e una volta che avrai una reputazione sufficiente sarai in grado di commentare qualsiasi post .
Argomento illegale

4
@IllegalArgument Si tratta di una risposta e non avrebbe senso come un commento. Tuttavia, non è la risposta migliore.
Qix - MONICA È STATA MISTREATA il

1
Questa risposta è difficile da seguire e non fornisce ragioni.
Venkat D.

2
Questo non spiega o risponde alla domanda.
Manjit Kumar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.