Wie gehe ich mit vielen-zu-vielen-Beziehungen in einer RESTful-API um?

288

Stellen Sie sich vor, Sie haben zwei Einheiten, Spieler und Team , in denen Spieler mehreren Teams angehören können. In meinem Datenmodell habe ich eine Tabelle für jede Entität und eine Verknüpfungstabelle, um die Beziehungen aufrechtzuerhalten. Hibernate kann damit gut umgehen, aber wie kann ich diese Beziehung in einer RESTful-API verfügbar machen?

Ich kann mir ein paar Möglichkeiten vorstellen. Erstens könnte jede Entität eine Liste der anderen enthalten, sodass ein Spielerobjekt eine Liste der Teams enthält, zu denen es gehört, und jedes Teamobjekt eine Liste der Spieler, die dazu gehören. Um einen Spieler zu einem Team hinzuzufügen, POSTEN Sie einfach die Darstellung des Spielers an einen Endpunkt, z. B. POST /playeroder POST, /teammit dem entsprechenden Objekt als Nutzlast der Anforderung. Dies scheint mir das "RESTful" zu sein, fühlt sich aber etwas komisch an.

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

Die andere Möglichkeit, dies zu tun, besteht darin, die Beziehung als eigenständige Ressource herauszustellen. /playerteam/team/{id}Um eine Liste aller Spieler in einem bestimmten Team anzuzeigen, können Sie ein GET oder ähnliches durchführen und eine Liste der PlayerTeam-Entitäten zurückerhalten. Um einen Spieler zu einem Team hinzuzufügen, POST /playerteammit einer entsprechend erstellten PlayerTeam-Entität als Nutzlast.

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

Was ist die beste Vorgehensweise dafür?

Richard Handarbeiter
quelle

Antworten:

129

In einer RESTful-Schnittstelle können Sie Dokumente zurückgeben, die die Beziehungen zwischen Ressourcen beschreiben, indem Sie diese Beziehungen als Links codieren. Man kann also sagen, dass ein Team eine Dokumentressource ( /team/{id}/players) hat, die eine Liste von Links zu Spielern ( /player/{id}) im Team ist, und ein Spieler kann eine Dokumentressource haben ()./player/{id}/teams) Dies ist eine Liste von Links zu Teams, bei denen der Spieler Mitglied ist. Schön und symmetrisch. Sie können die Kartenoperationen auf dieser Liste leicht genug ausführen, indem Sie einer Beziehung sogar eigene IDs zuweisen (wahrscheinlich haben sie zwei IDs, je nachdem, ob Sie zuerst über die Beziehung Team oder zuerst Spieler nachdenken), wenn dies die Sache einfacher macht . Das einzig schwierige ist, dass Sie daran denken müssen, die Beziehung auch am anderen Ende zu löschen, wenn Sie sie an einem Ende löschen. Sie müssen dies jedoch rigoros handhaben, indem Sie ein zugrunde liegendes Datenmodell verwenden und dann die REST-Schnittstelle anzeigen Dieses Modell wird das einfacher machen.

Beziehungs-IDs sollten wahrscheinlich auf UUIDs oder etwas ähnlich Langem und Zufälligem basieren, unabhängig davon, welche Art von IDs Sie für Teams und Spieler verwenden. Das wird Ihnen die gleiche UUID als ID - Komponente für jedes Ende der Beziehung nutzen , ohne sich Gedanken über Kollisionen (kleine ganze Zahlen Sie nicht haben diesen Vorteil). Wenn diese Mitgliedschaftsbeziehungen andere Eigenschaften haben als die bloße Tatsache, dass sie einen Spieler und ein Team bidirektional in Beziehung setzen, sollten sie eine eigene Identität haben, die sowohl von Spielern als auch von Teams unabhängig ist. Ein GET in der Teamansicht des Spielers ( /player/{playerID}/teams/{teamID}) könnte dann eine HTTP-Umleitung in die bidirektionale Ansicht ( /memberships/{uuid}) durchführen.

Ich empfehle, Links in alle XML-Dokumente zu schreiben, die Sie zurückgeben (falls Sie gerade XML produzieren), indem Sie XLink- xlink:href Attribute verwenden.

Donal Fellows
quelle
265

Erstellen Sie einen separaten Satz von /memberships/Ressourcen.

  1. Bei REST geht es darum, entwicklungsfähige Systeme zu entwickeln, wenn nichts anderes. In diesem Moment kann man nur darauf , dass ein bestimmter Spieler an einem bestimmten Team, aber irgendwann in der Zukunft, Sie werden diese Beziehung mit mehr Daten zu annotieren wollen: wie lange sie auf das Team waren, die sie bezeichnet an diese Mannschaft, wer ihr Trainer ist / war, während er in dieser Mannschaft ist, usw. usw.
  2. REST hängt aus Effizienzgründen vom Caching ab, was eine gewisse Berücksichtigung der Cache-Atomizität und -Invalidierung erfordert. Wenn Sie eine neue Entität in /teams/3/players/diese Liste POSTEN, wird sie ungültig, aber Sie möchten nicht, dass die alternative URL /players/5/teams/zwischengespeichert bleibt. Ja, verschiedene Caches haben Kopien jeder Liste mit unterschiedlichem Alter, und wir können nicht viel dagegen tun, aber wir können zumindest die Verwirrung für den Benutzer minimieren, der das Update veröffentlicht, indem wir die Anzahl der Entitäten begrenzen, die wir ungültig machen müssen im lokalen Cache ihres Kunden auf einen und nur einen bei /memberships/98745(siehe Hellands Diskussion über "alternative Indizes" in Life beyond Distributed Transactions für eine detailliertere Diskussion).
  3. Sie können die obigen 2 Punkte implementieren, indem Sie einfach /players/5/teamsoder /teams/3/players(aber nicht beide) auswählen . Nehmen wir das erstere an. Irgendwann möchten Sie jedoch /players/5/teams/eine Liste der aktuellen Mitgliedschaften reservieren und dennoch auf frühere Mitgliedschaften verweisen können . Erstellen Sie /players/5/memberships/eine Liste mit Hyperlinks zu /memberships/{id}/Ressourcen, und fügen Sie diese hinzu, /players/5/past_memberships/wenn Sie möchten, ohne dass alle Lesezeichen für die einzelnen Mitgliedschaftsressourcen gelöscht werden müssen. Dies ist ein allgemeines Konzept; Ich bin sicher, Sie können sich andere ähnliche Futures vorstellen, die für Ihren speziellen Fall besser geeignet sind.
Fumanchu
quelle
11
Punkt 1 und 2 sind perfekt erklärt, danke, wenn jemand mehr Fleisch für Punkt 3 in der Praxis hat, würde mir das helfen.
Alain
2
Beste und einfachste Antwort IMO danke! Zwei Endpunkte zu haben und sie synchron zu halten, hat eine Reihe von Komplikationen.
Venkat D.
7
Hallo Fumanchu. Fragen: Was bedeutet diese Nummer am Ende der URL im restlichen Endpunkt / Mitgliedschaften / 98745? Ist es eine eindeutige ID für die Mitgliedschaft? Wie würde man mit dem Endpunkt der Mitgliedschaft interagieren? Um einen Spieler hinzuzufügen, würde ein POST gesendet, der eine Nutzlast mit {team: 3, player: 6} enthält, wodurch die Verbindung zwischen den beiden hergestellt wird? Was ist mit einem GET? Würden Sie ein GET an / memberships? player = und / membersihps? team = senden, um Ergebnisse zu erhalten? Das ist die Idee? Vermisse ich etwas (Ich versuche, erholsame Endpunkte zu lernen.) Ist die ID 98745 in Mitgliedschaften / 98745 in diesem Fall jemals wirklich nützlich?
Aruuuuu
@aruuuuu Ein separater Endpunkt für eine Zuordnung sollte mit einer Ersatz-PK versehen werden. Es macht das Leben auch im Allgemeinen viel einfacher: / Mitgliedschaft / {Mitgliedschafts-ID}. Der Schlüssel (playerId, teamId) bleibt eindeutig und kann daher für die Ressourcen verwendet werden, die diese Beziehung besitzen: / team / {teamId} / player und / player / {playerId} / team. Es ist jedoch nicht immer so, dass solche Beziehungen auf beiden Seiten aufrechterhalten werden. Zum Beispiel Rezepte und Zutaten: Sie werden kaum jemals Zutaten / Zutaten / {Zutaten-ID} / Rezepte / verwenden müssen.
Alexander Palamarchuk
65

Ich würde eine solche Beziehung mit Subressourcen abbilden, allgemeines Design / Durchqueren wäre dann:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

In Restful-Begriffen hilft es sehr, nicht an SQL zu denken und sich zu verbinden, sondern mehr in Sammlungen, Untersammlungen und Traversal.

Einige Beispiele:

# 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

Wie Sie sehen, verwende ich POST nicht, um Spieler in Teams zu platzieren, sondern PUT, das Ihre n: n-Beziehung zwischen Spielern und Teams besser handhabt.

Manuel Aldana
quelle
20
Was ist, wenn team_player zusätzliche Informationen wie Status usw. hat? Wo vertreten wir es in Ihrem Modell? Können wir es zu einer Ressource machen und URLs dafür bereitstellen, genau wie bei game /, player /
Narendra Kamma
Hey, kurze Frage, nur um sicherzugehen, dass ich das richtig mache: GET / Teams / 1 / Spieler / 3 gibt einen leeren Antworttext zurück. Die einzig sinnvolle Antwort ist 200 gegen 404. Die Informationen der Spielerentität (Name, Alter usw.) werden NICHT von GET / Teams / 1 / Spieler / 3 zurückgegeben. Wenn der Kunde zusätzliche Informationen über den Spieler erhalten möchte, muss er / Spieler / 3 erhalten. Ist das alles richtig?
Verdagon
2
Ich stimme Ihrer Zuordnung zu, habe aber eine Frage. Es ist eine Frage der persönlichen Meinung, aber was denkst du über POST / Teams / 1 / Spieler und warum benutzt du es nicht? Sehen Sie einen Nachteil / eine Irreführung in diesem Ansatz?
JakubKnejzlik
2
POST ist nicht idempotent, dh wenn Sie POST / Teams / 1 / Spieler n-mal machen, würden Sie n-mal / Teams / 1 ändern. Wenn Sie jedoch einen Spieler n-mal nach / team / 1 verschieben, ändert sich der Status des Teams nicht, sodass die Verwendung von PUT offensichtlicher ist.
Manuel Aldana
1
@NarendraKamma Ich nehme an, nur statusals Parameter in PUT-Anfrage senden ? Gibt es einen Nachteil bei diesem Ansatz?
Traxo
22

Die vorhandenen Antworten erklären nicht die Rollen von Konsistenz und Idempotenz - die ihre Empfehlungen von UUIDs/ Zufallszahlen für IDs und PUTstattdessen motivieren POST.

Wenn wir den Fall betrachten, in dem wir ein einfaches Szenario wie " Hinzufügen eines neuen Spielers zu einem Team " haben, treten Konsistenzprobleme auf.

Da der Spieler nicht existiert, müssen wir:

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

Sollte der Client-Vorgang jedoch nach dem POSTto fehlschlagen /players, haben wir einen Spieler erstellt, der keinem Team angehört:

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

Jetzt haben wir einen verwaisten doppelten Spieler in /players/5.

Um dies zu beheben, schreiben wir möglicherweise einen benutzerdefinierten Wiederherstellungscode, der nach verwaisten Spielern sucht, die einem natürlichen Schlüssel entsprechen (z Name. B. ). Dies ist benutzerdefinierter Code, der getestet werden muss, mehr Geld und Zeit kostet usw. usw.

Um zu vermeiden, dass benutzerdefinierter Wiederherstellungscode benötigt wird, können wir PUTstattdessen implementieren POST.

Aus dem RFC :

Die Absicht von PUTist idempotent

Damit eine Operation idempotent ist, müssen externe Daten wie vom Server generierte ID-Sequenzen ausgeschlossen werden. Aus diesem Grund empfehlen die Leute beide PUTund UUIDs für Ids zusammen.

Dies ermöglicht es uns, sowohl das /players PUTals auch das /memberships PUTohne Konsequenzen erneut auszuführen :

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

Alles ist in Ordnung und wir mussten nichts weiter tun, als es für Teilausfälle erneut zu versuchen.

Dies ist eher ein Nachtrag zu den bestehenden Antworten, aber ich hoffe, dass sie in den Kontext des Gesamtbildes gestellt werden, wie flexibel und zuverlässig ReST sein kann.

Seth
quelle
Woher haben Sie an diesem hypothetischen Endpunkt das 23lkrjrqwlej?
cbcoutinho
1
Roll Face auf der Tastatur - es gibt nichts Besonderes an dem 23lkr ... Gobbledegook, außer dass es nicht sequentiell oder sinnvoll ist
Seth
9

Meine bevorzugte Lösung besteht darin, drei Ressourcen zu erstellen : Players, Teamsund TeamsPlayers.

Um alle Spieler eines Teams zu gewinnen, gehen Sie einfach zur TeamsRessource und rufen Sie alle Spieler an GET /Teams/{teamId}/Players.

Um alle Teams zu erhalten, die ein Spieler gespielt hat, erhalten Sie die TeamsRessource innerhalb der Players. Rufen Sie an GET /Players/{playerId}/Teams.

Und um die Viele-zu-Viele-Beziehung anzurufen GET /Players/{playerId}/TeamsPlayersoder GET /Teams/{teamId}/TeamsPlayers.

Beachten Sie, dass Sie in dieser Lösung beim Aufrufen GET /Players/{playerId}/Teamseine Reihe von TeamsRessourcen erhalten, die genau dieselbe Ressource sind, die Sie beim Aufrufen erhalten GET /Teams/{teamId}. Das Gegenteil folgt dem gleichen Prinzip: PlayersBeim Aufruf erhalten Sie eine Reihe von Ressourcen GET /Teams/{teamId}/Players.

Bei beiden Aufrufen werden keine Informationen zur Beziehung zurückgegeben. Beispielsweise wird no contractStartDatezurückgegeben, da die zurückgegebene Ressource keine Informationen über die Beziehung enthält, sondern nur über ihre eigene Ressource.

Um mit der nn-Beziehung umzugehen, rufen Sie entweder GET /Players/{playerId}/TeamsPlayersoder an GET /Teams/{teamId}/TeamsPlayers. Diese Aufrufe geben die genaue Ressource zurück TeamsPlayers.

Diese TeamsPlayersRessource hat id, playerId, teamIdAttribute, sowie einige andere , die die Beziehung zu beschreiben. Außerdem verfügt es über die erforderlichen Methoden, um mit ihnen umzugehen. GET, POST, PUT, DELETE usw., die die Beziehungsressource zurückgeben, einschließen, aktualisieren und entfernen.

Die TeamsPlayersRessource implementiert einige Abfragen, z. B. GET /TeamsPlayers?player={playerId}um alle TeamsPlayersBeziehungen zurückzugeben, die der Spieler identifiziert {playerId}hat. Verwenden Sie GET /TeamsPlayers?team={teamId}nach der gleichen Idee, um alle TeamsPlayersim {teamId}Team gespielten Daten zurückzugeben. Bei beiden GETAufrufen wird die Ressource TeamsPlayerszurückgegeben. Alle Daten, die sich auf die Beziehung beziehen, werden zurückgegeben.

Beim Aufrufen GET /Players/{playerId}/Teams(oder GET /Teams/{teamId}/Players) ruft die Ressource Players(oder Teams) TeamsPlayersauf, um die zugehörigen Teams (oder Spieler) mithilfe eines Abfragefilters zurückzugeben.

GET /Players/{playerId}/Teams funktioniert so:

  1. Finde alle TeamsPlayers , für die der Spieler die ID = playerId hat . ( GET /TeamsPlayers?player={playerId})
  2. Schleife die zurückgegebenen TeamsPlayers
  3. Unter Verwendung der Mannschafts - ID erhalten Sie TeamsPlayers , Anruf GET /Teams/{teamId}und speichert die zurückgegebenen Daten
  4. Nachdem die Schleife beendet ist. Geben Sie alle Teams zurück, die in der Schleife waren.

Sie können denselben Algorithmus verwenden, um alle Spieler aus einem Team zu holen, wenn Sie anrufen GET /Teams/{teamId}/Players, aber Teams und Spieler austauschen.

Meine Ressourcen würden folgendermaßen aussehen:

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

Diese Lösung basiert nur auf REST-Ressourcen. Obwohl einige zusätzliche Aufrufe erforderlich sein können, um Daten von Spielern, Teams oder deren Beziehung abzurufen, lassen sich alle HTTP-Methoden problemlos implementieren. POST, PUT, DELETE sind einfach und unkompliziert.

Jedes Mal, wenn eine Beziehung erstellt, aktualisiert oder gelöscht wird, werden beide Playersund TeamsRessourcen automatisch aktualisiert.

Haroldo Macedo
quelle
Es ist wirklich sinnvoll, die TeamPlayers-Ressource
vorzustellen. Super
beste Erklärung
Diana
1

Ich weiß, dass es eine Antwort gibt, die als akzeptiert für diese Frage markiert ist. Hier ist jedoch, wie wir die zuvor aufgeworfenen Probleme lösen können:

Sagen wir für PUT

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

Beispielsweise führen die folgenden Schritte alle zum gleichen Effekt, ohne dass eine Synchronisierung erforderlich ist, da sie für eine einzelne Ressource ausgeführt werden:

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

Wenn wir nun mehrere Mitgliedschaften für ein Team aktualisieren möchten, können wir Folgendes tun (mit den richtigen Validierungen):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}
Heidar Pirzadeh
quelle
-3
  1. / Spieler (ist eine Master-Ressource)
  2. / Teams / {ID} / Spieler (ist eine Beziehungsressource, daher reagiert es anders als 1)
  3. / Mitgliedschaften (ist eine Beziehung, aber semantisch kompliziert)
  4. / Spieler / Mitgliedschaften (ist eine Beziehung, aber semantisch kompliziert)

Ich bevorzuge 2

MoaLaiSkirulais
quelle
2
Vielleicht verstehe ich die Antwort einfach nicht, aber dieser Beitrag scheint die Frage nicht zu beantworten.
BradleyDotNET
Dies gibt keine Antwort auf die Frage. Um einen Autor zu kritisieren oder um Klärung zu bitten, hinterlassen Sie einen Kommentar unter seinem Beitrag. Sie können jederzeit Ihre eigenen Beiträge kommentieren. Sobald Sie einen ausreichenden Ruf haben, können Sie jeden Beitrag kommentieren .
Illegales Argument
4
@IllegalArgument Es ist eine Antwort und würde als Kommentar keinen Sinn ergeben. Es ist jedoch nicht die beste Antwort.
Qix - MONICA wurde am
1
Diese Antwort ist schwer zu folgen und liefert keine Gründe.
Venkat D.
2
Dies erklärt oder beantwortet die gestellte Frage überhaupt nicht.
Manjit Kumar