Come gestire molti-a-molti rapporti in un RESTful API?

Immagina di avere 2 entità, Giocatore e Team, dove i giocatori possono essere su più squadre. Nel mio modello di dati, ho una tabella per ogni entità, e una tabella di join mantenere i rapporti. Hibernate è la manipolazione di questo, ma come potrei esporre questo rapporto in un RESTful API?

Posso pensare a un paio di modi. Prima, potrei avere ogni entità contengono un elenco di altri, in modo che un Giocatore oggetto, un elenco di Squadre di appartenenza, e ogni Squadra in oggetto, l’elenco dei Giocatori che ne fanno parte. In modo da aggiungere un Giocatore di una Squadra, si sarebbe solo un POST di un giocatore rappresentazione di un endpoint, qualcosa di simile POST /player o POST /team con l’oggetto appropriato come il payload della richiesta. Questo mi sembra il più “Tranquillo”, ma mi 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 che posso pensare di farlo sarebbe quello di esporre la relazione come risorsa in sé. Quindi per vedere un elenco di tutti i giocatori di una squadra, si potrebbe fare un /playerteam/team/{id} o qualcosa del genere e ottenere un elenco di PlayerTeam entità. Per aggiungere un giocatore di una squadra, POST /playerteam con un adeguatamente costruito PlayerTeam entità del carico.

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

Che cosa è la pratica migliore per questo?

 

7 Replies
  1. 115

    In un’interfaccia RESTful, è possibile restituire i documenti che descrivono le relazioni tra le risorse mediante la codifica di tali relazioni come collegamenti. Così, una squadra può essere detto di avere un documento di risorse (/team/{id}/players) che è un elenco di link ai giocatori (/player/{id}) sulla squadra, e un giocatore può avere un documento di risorse (/player/{id}/teams) che è un elenco di link alle squadre che il giocatore è un membro di. Bella e simmetrica. È possibile mappa di operazioni sulla lista abbastanza facilmente, dando anche un rapporto proprio Id (probabilmente l’avrebbero due Id, a seconda se si sta pensando a rapporto squadra-prima o giocatore) se il che rende le cose più facili. Il solo difficile bit è che bisogna ricordarsi di eliminare la relazione per l’altra estremità, se si elimina dall’inizio alla fine, ma rigorosamente la manipolazione di questo utilizzando un modello di dati sottostante e quindi l’interfaccia REST essere una vista di quel modello sta per rendere questo più facile.

    Rapporto di Id, probabilmente, dovrebbe essere basata su Uuid o qualcosa di altrettanto lunga e casuale, a prescindere da qualsiasi tipo di Id utilizzata per squadre e giocatori. Che vi permetterà di utilizzare lo stesso UUID ID componente per ogni termine del rapporto, senza preoccuparsi di collisioni (piccoli numeri interi fare non hanno quel vantaggio). Se questi appartenenza rapporti di qualsiasi proprietà diverse rispetto a nudo il fatto che, trattandosi di un giocatore e di una squadra in un bidirezionale moda, si dovrebbe avere una propria identità indipendente di entrambi i giocatori e le squadre; una sul giocatore»squadra vista (/player/{playerID}/teams/{teamID}) potrebbe poi fare un redirect HTTP al bidirezionale vista (/memberships/{uuid}).

    Mi consiglia di scrivere collegamenti in qualsiasi documenti XML di ritorno (se vi capita di essere la produzione di XML, naturalmente) utilizzando XLink xlink:href attributi.

  2. 246

    Fare un set separato di /memberships/ risorse.

    1. RESTO è su sistemi in grado di evolversi, se non altro. In questo momento, è possibile solo la cura che un giocatore è in una squadra, ma a un certo punto, in futuro, si sarà desidera annotare che il rapporto con ulteriori dati: per quanto tempo sono stati in quella squadra, che li ha riferiti a questa squadra, che il loro allenatore è/era, mentre in quella squadra, ecc ecc.
    2. Il RESTO dipende dal memorizzazione nella cache per l’efficienza, il che richiede un po ‘ di considerazione per la cache di atomicità e nullità. Se si POSTA una nuova entità di /teams/3/players/ tale elenco sarà invalidata, ma non si desidera che la URL alternativo /players/5/teams/ rimangono memorizzati nella cache. Sì, cache diverse avranno copie di ciascuna lista con età diverse, e non c’è molto che possiamo fare al riguardo, ma si può almeno ridurre al minimo la confusione per l’utente POST ing aggiornamento, limitando il numero di soggetti abbiamo bisogno di invalidare nel loro client della cache locale per uno e un solo a /memberships/98745 (vedi Helland discussione di “alternativo indici” in La vita al di là di Transazioni Distribuite per una discussione più dettagliata).
    3. Si potrebbe implementare al di sopra di 2 punti, semplicemente scegliendo /players/5/teams o /teams/3/players (ma non entrambi). Supponiamo che l’ex. A un certo punto, tuttavia, si desidera prenotare /players/5/teams/ per un elenco di corrente appartenenze, e ancora in grado di fare riferimento a passato appartenenze da qualche parte. Fare /players/5/memberships/ un elenco di collegamenti ipertestuali per /memberships/{id}/ risorse, e quindi è possibile aggiungere /players/5/past_memberships/ che vuoi, quando vuoi, senza dover rompere tutti i segnalibri per l’iscrizione individuale risorse. Questo è un concetto generale; sono sicuro che si può immaginare di altri simili futures che sono più applicabili al caso specifico.
    • Brillante, grazie per questa risposta!
    • Punto 1 & 2 sono perfettamente spiegato, grazie, se qualcuno ha più carne per il punto 3, di esperienza di vita reale, che mi avrebbe aiutato.
    • Migliore e più semplice rispondere IMO grazie! Avendo due endpoint e il loro mantenimento in sync ha una serie di complicazioni.
    • ciao fumanchu. Domande: l’endpoint rest /appartenenza/98745 che cosa numero alla fine dell’url rappresentano? È un id univoco per l’iscrizione? Come potrebbe interagire con le appartenenze endpoint? Per aggiungere un giocatore di un POST inviato contenente un carico utile di { team : 3, player : 6 } , creando così un collegamento tra i due? Che cosa circa OTTENERE ? si prega di mandare una a /appartenenze?giocatore= e /membersihps?squadra= per ottenere i risultati ? Che è l’idea? Mi manca qualcosa? (Sto cercando di imparare l’endpoint restful) In questo caso, è l’id 98745 appartenenze/98745 mai veramente utile?
    • un endpoint separato per un’associazione dovrebbe essere fornito con un surrogato di PK. Rende la vita molto più facile, in generale, anche: /appartenenza/{membershipId}. La chiave (playerId, idteam) rimane unico e quindi può essere utilizzato le risorse che possiede questa relazione: /squadre/{idteam}/e lettori /giocatori/{playerId}/squadre. Ma non sempre quando tali rapporti si sono mantenuti su entrambi i lati. Per esempio, le Ricette e gli Ingredienti: è quasi mai bisogno di usare /ingredienti/{ingredientId}/ricette/.
  3. 59

    Vorrei una mappa di tali relazioni con sub-risorse generali di progettazione/di attraversamento, quindi, sarebbe:

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

    Riposo-termini aiuta molto a non pensare di SQL e join ma più in raccolte sotto-collezioni e di attraversamento.

    Alcuni esempi:

    # 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 io non uso il POST per mettere i giocatori a squadre, ma METTERE, che gestisce la relazione n:n di giocatori e squadre meglio.

    • Che cosa succede se team_player è di avere ulteriori informazioni sullo status ecc? dove possiamo rappresentare in un modello? possiamo promuovere una risorsa, e di fornire l’Url per questo, solo come gioco/ giocatore/
    • Ehi, domanda veloce, solo per essere sicuri di ottenere questo diritto: GET /squadre/1/giocatori/3 restituisce una risposta vuota corpo. L’unica risposta significativa da questo è di 200 vs 404. Il giocatore entità di informazioni (nome, età, ecc) NON è restituito da OTTENERE /squadre/1/giocatori/3. Se il cliente vuole ottenere ulteriori informazioni sul giocatore, egli deve OTTENERE /giocatori/3. Questo è tutto corretto?
    • Manuale, Qualsiasi motivo non uso /giocatori e /player in cui è necessario per ottenere tutti o di ottenere un risultato(s)?
    • Sì, se /squadre/1/giocatori/3 restituisce 404 (i giocatori non appartiene a squadra o ha smesso), può tornare in 200 per /giocatori/3.
    • Sono d’accordo con la mappatura, ma hanno una domanda. E ‘ questione di opinioni personali, ma cosa pensi del POST /squadre/1/giocatori e perché non usarlo? Non si vede alcun svantaggio/fuorviante in questo approccio?
    • Il POST non è idempotente, cioè se il POST /squadre/1/giocatori di n-volte, cambiare di n-volte /squadre/1. ma lo spostamento di un giocatore a /squadre/1 n-volte a non cambiare il team è stato così con la METTI è più evidente.
    • Presumo che basta inviare status come param MESSO a richiesta? C’è un aspetto negativo di questo approccio?
    • Risposta eccellente. Grazie.
    • POST /squadre/:idteam/giocatori potrebbero essere considerati di supporto per l’aggiunta di più giocatori alla volta, mettendo il giocatore id in un array nel corpo. Tuttavia, non sarebbe simmetrica in quanto ELIMINA non può essere utilizzato per rimuovere più di un giocatore da una squadra. Preferirei usare il design ” in questa risposta, ed eventualmente integrare con il PUT /squadre/:idteam/giocatori in cui l’intero elenco di giocatori in una squadra potrebbe essere sostituito, quindi il supporto di più le aggiunte e rimozioni (a OTTENERE seguito da un MESSO). Per la maggior parte delle situazioni l’endpoint in questa risposta, sono probabilmente abbastanza però.
    • cosa succede se il mio cliente vuole fornire una tabella per i nuovi giocatori per questa squadra. così il datatable deve contenere solo i giocatori che non fanno parte di una squadra. come potrebbe questo endpoint essere raggiungibile? sto cercando qualcosa di simile /team/1/notplayers
    • forse /team/1/exclude/players sarebbe estremità destra?

  4. 17

    Esistenti risposte non spiegare i ruoli di coerenza e di idempotenza – che motivano le loro raccomandazioni di UUIDs/numeri casuali per Id e PUT invece di POST.

    Se consideriamo il caso in cui si dispone di un semplice scenario come “Aggiungere un nuovo giocatore di una squadra“, ci imbattiamo in problemi di coerenza.

    Perché il giocatore non esiste, abbiamo bisogno di:

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

    Tuttavia, qualora il cliente operazione non dopo che il POST per /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 orfano duplicato giocatore in /players/5.

    Per risolvere questo problema è possibile scrivere il recupero personalizzato il codice che controlla orfani giocatori che corrispondono naturale chiave (ad esempio Name). Questo è il codice personalizzato che deve essere testato, costa di più tempo e di denaro, ecc ecc

    Per evitare di doverlo personalizzato il codice di recupero, siamo in grado di realizzare PUT invece di POST.

    Dal RFC:

    l’intento di PUT è idempotente

    Per un’operazione da idempotente, si deve escludere dati esterni come quelli generati dal server id sequenze. Questo è il motivo per cui le persone stanno raccomandando sia PUT e UUIDs per Ids insieme.

    Questo ci permette di eseguire sia il /players PUT e il /memberships PUT senza conseguenze:

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

    Tutto bene e non abbiamo bisogno di fare qualcosa di più che tentativi per guasti parziali.

    Questo è più di un atto aggiuntivo alla vigente risposte, ma spero che li inserisce nel contesto di un quadro più ampio di quanto flessibile e affidabile Resto può essere.

  5. 5

    La mia soluzione preferita è quella di creare tre risorse: Players, Teams e TeamsPlayers.

    Così, per ottenere tutti i giocatori di una squadra, basta andare Teams di risorse e di ottenere tutti i suoi giocatori chiamando GET /Teams/{teamId}/Players.

    D’altra parte, per ottenere tutte le squadre un giocatore ha giocato, ottenere il Teams risorsa all’interno del Players. Chiamata GET /Players/{playerId}/Teams.

    E-per molti-a-molti rapporti chiamata GET /Players/{playerId}/TeamsPlayers o GET /Teams/{teamId}/TeamsPlayers.

    Di notare che, in questa soluzione, quando si chiama GET /Players/{playerId}/Teams, si ottiene una matrice di Teams risorse, che è esattamente la stessa risorsa si ottiene quando si chiama GET /Teams/{teamId}. Inverso segue lo stesso principio, si ottiene una matrice di Players risorse quando la chiamata GET /Teams/{teamId}/Players.

    In entrambe le chiamate, nessuna informazione circa il rapporto è tornato. Per esempio, non contractStartDate è tornata, perché la risorsa restituito non dispone di informazioni circa il rapporto, solo sulle sue risorse proprie.

    Per affrontare con la n-n relazione, chiamare GET /Players/{playerId}/TeamsPlayers o GET /Teams/{teamId}/TeamsPlayers. Queste chiamate di ritorno esattamente la risorsa, TeamsPlayers.

    Questo TeamsPlayers risorsa ha id, playerId, teamId attributi, così come alcuni altri per descrivere il rapporto. Inoltre, i metodi necessari per trattare con loro. GET, POST, PUT, DELETE, ecc. che tornerà, includere, aggiornare, rimuovere la relazione di risorse.

    Il TeamsPlayers risorsa, implementa alcune query, come GET /TeamsPlayers?player={playerId} per restituire tutti TeamsPlayers relazioni con il giocatore individuato da {playerId} ha. In seguito la stessa idea, utilizzare GET /TeamsPlayers?team={teamId} per restituire tutti i TeamsPlayers che hanno giocato nel {teamId} squadra.
    In entrambi i GET chiamata, la risorsa TeamsPlayers viene restituito. Tutti i dati relativi al rapporto viene restituito.

    Quando si chiama GET /Players/{playerId}/Teams (o GET /Teams/{teamId}/Players), la risorsa Players (o Teams) chiamate TeamsPlayers per restituire le relative squadre (o i giocatori) che utilizza un filtro di query.

    GET /Players/{playerId}/Teams funziona in questo modo:

    1. Trovare tutti TeamsPlayers che il giocatore ha id = playerId. (GET /TeamsPlayers?player={playerId})
    2. Loop restituito TeamsPlayers
    3. Utilizzando il idteam ottenuto da TeamsPlayers, chiamata GET /Teams/{teamId} e memorizzare i dati restituiti
    4. Dopo il ciclo termina. Di ritorno tutte le squadre che non hanno avuto il ciclo.

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

    Mie risorse sarebbe simile a questa:

    /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 su risorse RESTO solo. Anche se alcune chiamate extra può essere necessario per ottenere i dati da giocatori, squadre o il loro rapporto, tutti i metodi HTTP sono di facile attuazione. POST, PUT, DELETE, sono semplici e lineari.

    Ogni volta che si crea una relazione, aggiornati o cancellati, sia Players e Teams risorse vengono aggiornate automaticamente.

    • ha davvero senso introdurre TeamsPlayers risorsa.Impressionante
  6. 1

    So che la risposta c’è contrassegnato come accettato per questa domanda, tuttavia, qui è come si può risolvere il già sollevato questioni:

    Diciamo per METTERE

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

    Come un esempio, i seguenti punti si ottiene lo stesso effetto, senza che sia necessario per la sincronizzazione perché sono fatto su una singola risorsa:

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

    ora, se si desidera aggiornare appartenenze multiple per una squadra che non segue (con la giusta convalide):

    PUT    /membership/teams/team1/
    
    {
        membership: [
            {
                teamId: "team1"
                playerId: "player1"
            },
            {
                teamId: "team1"
                playerId: "player2"
            },
            ...
        ]
    }
    
  7. -3
    1. /giocatori (è un master in risorse)
    2. /squadre/{id}/giocatori (è una relazione di risorse, in modo da reagire differenti: 1)
    3. /iscrizioni (è un rapporto, ma semanticamente complicato)
    4. /giocatori/iscrizioni (è un rapporto, ma semanticamente complicato)

    Preferisco 2

    • Forse non riesco a capire la risposta, ma questo post non sembra rispondere alla domanda.
    • Questo non fornisce una risposta alla domanda. Per una critica o richiesta di chiarimento da parte di un autore, lasciare un commento sotto il post – si può sempre commentare i tuoi post, e una volta che si dispone di sufficiente reputazione si sarà in grado di commentare qualsiasi post.
    • È una risposta e non avrebbe senso come un commento. Tuttavia, non è la più grande risposta.
    • Questa risposta è difficile da seguire e non di fornire ragioni.
    • Questo non spiega o risponde alla domanda a tutti.
    • Questo risponde anche alla domanda in pieno se si capisce il dominio del problema. Purtroppo, sembra che MoaLai non è un madrelingua inglese, quindi non può elaborare facilmente.

Lascia un commento