RESTful APIで多対多の関係を処理する方法は?


288

PlayerTeamの 2つのエンティティがあり、プレーヤーが複数のチームに所属しているとします。私のデータモデルでは、各エンティティのテーブルと、関係を維持するための結合テーブルがあります。Hibernateはこれをうまく処理できますが、この関係をRESTful APIでどのように公開できますか?

いくつかの方法を考えることができます。最初に、各エンティティに他のエンティティのリストを含めることができるため、Playerオブジェクトには所属するチームのリストがあり、各チームオブジェクトには所属するプレーヤーのリストがあります。そのため、プレーヤーをチームに追加するには、プレーヤーの表現をエンドポイントにPOSTします。たとえば、POST /playerやPOSTのよう/teamに、リクエストのペイロードとして適切なオブジェクトを指定します。これは私にとって最も「RESTful」なようですが、少し変な感じがします。

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

これを行うために私が考えることができるもう1つの方法は、それ自体がリソースとして関係を公開することです。そのため、特定のチームのすべてのプレーヤーのリストを表示するには、GET /playerteam/team/{id}などを実行して、PlayerTeamエンティティのリストを取得します。プレーヤーをチームに追加するに/playerteamは、適切にビルドされたPlayerTeamエンティティをペイロードとしてPOST します。

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

これのベストプラクティスは何ですか?

回答:


129

RESTfulインターフェースでは、リソース間の関係をリンクとしてエンコードすることにより、それらの関係を記述したドキュメントを返すことができます。したがって、チームには、チームの/team/{id}/playersプレーヤー(/player/{id})へのリンクのリストであるドキュメントリソース()があると言うことができ、プレーヤーはドキュメントリソース(/player/{id}/teams)これは、プレーヤーがメンバーであるチームへのリンクのリストです。素敵で対称的です。そのリストのマップ操作を十分に簡単に行うことができ、関係に独自のIDを与えることもできます(おそらく、関係をチームファーストにするか、プレーヤーファーストにするかによって、2つのIDが必要になります)。 。唯一のトリッキーなビットは、関係を一方の端から削除する場合は、もう一方の端からも削除することを忘れないようにする必要があることですが、基礎となるデータモデルを使用してこれを厳密に処理し、RESTインターフェイスをそのモデルはそれを簡単にします。

リレーションシップIDは、チームやプレーヤーに使用するIDのタイプに関係なく、UUIDまたは同様に長くランダムなものに基づいているはずです。これにより、衝突を心配せずに、関係の両端に同じUUIDをIDコンポーネントとして使用できます(小さな整数にはその利点ありませ)。これらのメンバーシップ関係に双方向の方法でプレーヤーとチームを関連付けるという裸の事実以外の特性がある場合、それらはプレーヤーとチームの両方から独立した独自のアイデンティティを持つ必要があります。プレーヤー»チームビュー(/player/{playerID}/teams/{teamID})でGETを実行すると、双方向ビュー(/memberships/{uuid})へのHTTPリダイレクトを実行できます。

XLink xlink:href属性を使用して、返すXMLドキュメントにリンクを記述することをお勧めします(もちろん、XMLを生成している場合)。


265

別の/memberships/リソースセットを作成します。

  1. RESTは、他に何もなければ進化可能なシステムを作ることです。この時点では、特定のプレーヤーが特定のチームに所属していることのみを気にする場合がありますが、将来のある時点で、その関係をより多くのデータで注釈付けする必要があります。そのチームに、彼らのコーチが誰であるか/そのチームにいた間などなど
  2. RESTは効率をキャッシングに依存しているため、キャッシュのアトミック性と無効化を考慮する必要があります。/teams/3/players/そのリストへの新しいエンティティをPOSTすると無効になりますが、代替URL /players/5/teams/をキャッシュしたままにしたくない場合。はい、キャッシュごとに各リストのコピーが保存され、保存期間も異なりますが、これについてできることはほとんどありませんが、無効にする必要があるエンティティの数を制限することで、ユーザーが更新をPOSTする際の混乱を最小限に抑えることができますクライアントのローカルキャッシュを1つだけにします/memberships/98745(詳細については、Hellandの「分散トランザクション超え生命」の「代替インデックス」に関する説明を参照してください)。
  3. 上記の2つのポイントを実装するには、単に/players/5/teamsまたは/teams/3/players(両方ではなく)を選択します。前者を想定しましょう。ただし、ある時点で、現在のメンバーシップの/players/5/teams/リストを予約しておきながら、どこかで過去のメンバーシップを参照できるようにする必要があります。作りへのハイパーリンクのリスト資源を、その後、あなたは追加することができますあなたが好きな時に、個々のメンバーシップのリソースのためにみんなのブックマークを破ることなく、。これは一般的な概念です。あなたはあなたの特定のケースにより適した他の同様の未来を想像できると思います。/players/5/memberships//memberships/{id}//players/5/past_memberships/

11
ポイント1と2は完全に説明されています。実際の経験でポイント3の肉をもっと多く持っている人がいれば、それが役に立ちます。
アラン

2
最善かつ最も簡単な回答IMOに感謝します!2つのエンドポイントがあり、それらの同期を維持することには、多くの複雑な問題があります。
Venkat D.

7
こんにちはフマンチュ。質問:残りのエンドポイント/ memberships / 98745では、URLの最後の数字は何を表していますか?メンバーシップの一意のIDですか?メンバーシップエンドポイントとどのように相互作用しますか?プレーヤーを追加するには、{team:3、player:6}のペイロードを含むPOSTを送信し、それによって2つの間にリンクを作成しますか?GETについてはどうですか?結果を取得するには、GETを/ memberships?player =および/ membersihps?team =に送信しますか?それはアイデアですか?何か不足していますか?(私は安らかなエンドポイントを学習しようとしています)その場合、memberships / 98745のID 98745は本当に便利ですか?
aruuuuu 2017年

@aruuuuu関連付けの個別のエンドポイントには、代理PKを提供する必要があります。また、一般的に/ memberships / {membershipId}を使用すると、作業が大幅に簡単になります。キー(playerId、teamId)は一意のままであるため、この関係を持つリソース(/ teams / {teamId} / playersおよび/ players / {playerId} / teams)で使用できます。しかし、そのような関係が双方で維持されているとは限りません。たとえば、レシピと材料:/ ingredients / {ingredientId} / recipes /を使用する必要はほとんどありません。
Alexander Palamarchuk

65

そのような関係をサブリソースとマッピングすると、一般的な設計/トラバーサルは次のようになります。

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

Restful用語では、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を使用していませんが、プレーヤーとチームのn:n関係をより適切に処理するPUTを使用しています。


20
team_playerにステータスなどの追加情報がある場合はどうなりますか?私たちはあなたのモデルのどこにそれを表していますか?それをリソースにプロモートして、それにgame /、player /のようにURLを提供できますか
Narendra Kamma

こんにちは、私がこれが正しく行われていることを確認するための簡単な質問です。GET/ teams / 1 / players / 3は空の応答本文を返します。これからの唯一の意味のある応答は200対404です。プレーヤーエンティティの情報(名前、年齢など)は、GET / teams / 1 / players / 3によって返されません。クライアントがプレーヤーに関する追加情報を取得する場合は、/ players / 3を取得する必要があります。これはすべて正しいですか?
Verdagon、2013年

2
私はあなたのマッピングに同意しますが、1つの質問があります。それは個人的な意見の問題ですが、POST / teams / 1 / playersについてどう思いますか、そしてなぜそれを使わないのですか?このアプローチに不利な点や誤解を招く点がありますか?
JakubKnejzlik 2014

2
POSTはべき等ではありません。つまり、POST / teams / 1 / playersをn回行うと、n回/ teams / 1を変更します。ただし、プレーヤーを/ teams / 1にn回移動しても、チームの状態は変更されないため、PUTを使用する方がわかりやすくなります。
manuel aldana

1
@NarendraKamma statusPUTリクエストでパラメーターとして送信するだけだと思いますか?そのアプローチには欠点がありますか?
Traxo

22

既存の回答は、一貫性とべき等性の役割を説明していません。これはUUIDs、IDの/乱数の推奨を動機付けています。PUTPOST

新しいプレーヤーをチームに追加する」のような単純なシナリオがある場合を考えると、一貫性の問題が発生します。

プレーヤーが存在しないため、次のことを行う必要があります。

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

ただし、POSTtoの後にクライアントの操作が失敗した/players場合は、チームに属していないプレーヤーを作成しました。

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シーケンスなどの外部データを除外する必要があります。これが、人々がPUTUUIDの両方をId一緒に推奨している理由です。

これにより、/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がいかに柔軟で信頼性の高いものであるかという全体像の文脈でそれらが役立つことを願っています。


この架空のエンドポイントで、どこ23lkrjrqwlejから入手しましたか?
cbcoutinho

1
キーボードで顔を転がす-23lkrには特別なものはありません... gobbledegookは、連続的でも意味のあるものでもないことを除きます
セス

9

私の好適な解決策は、3つのリソースを作成することです:PlayersTeamsTeamsPlayers

したがって、チームのすべてのプレーヤーを取得するには、Teamsリソースに移動し、を呼び出してすべてのプレーヤーを取得しますGET /Teams/{teamId}/Players

一方、プレーヤーがプレイしたすべてのチームを取得するには、Teams内のリソースを取得しますPlayers。を呼び出しGET /Players/{playerId}/Teamsます。

そして、多対多の関係の呼び出しを取得するには、GET /Players/{playerId}/TeamsPlayersまたはGET /Teams/{teamId}/TeamsPlayers

このソリューションでは、を呼び出すとGET /Players/{playerId}/TeamsTeamsリソースの配列を取得しますGET /Teams/{teamId}。これは、を呼び出すときに取得するリソースとまったく同じです。逆も同じ原理に従い、をPlayers呼び出すと、リソースの配列を取得しますGET /Teams/{teamId}/Players

どちらの呼び出しでも、関係に関する情報は返されません。たとえば、contractStartDate返されるリソースには関係に関する情報がなく、自分のリソースに関する情報しかないため、no は返されません。

nn関係を処理するには、GET /Players/{playerId}/TeamsPlayersまたはを呼び出しますGET /Teams/{teamId}/TeamsPlayers。これらの呼び出しは、正確なリソースを返しTeamsPlayersます。

このTeamsPlayersリソースがありidplayerIdteamId関係を記述するための属性だけでなく、いくつか他の人を。また、それらに対処するために必要なメソッドがあります。GET、POST、PUT、DELETEなど、関係リソースを返し、組み込み、更新、削除します。

TeamsPlayersリソースの実装いくつかのクエリは、好きなGET /TeamsPlayers?player={playerId}すべて返すことTeamsPlayersで識別されるプレイヤーは関係{playerId}あります。同じ考えに従って、を使用GET /TeamsPlayers?team={teamId}TeamsPlayersて、{teamId}チームでプレイしたすべてのを返します。どちらのGET呼び出しでも、リソースTeamsPlayersが返されます。関係に関連するすべてのデータが返されます。

呼び出すときGET /Players/{playerId}/Teams(またはGET /Teams/{teamId}/Players)、リソースがPlayers(またはTeams)を呼び出すTeamsPlayersクエリフィルタを使用して、関連チーム(またはプレイヤー)を返すように。

GET /Players/{playerId}/Teams このように動作します:

  1. プレーヤーid = playerIdであるすべてのTeamsPlayersを検索します。()GET /TeamsPlayers?player={playerId}
  2. 返されたTeamsPlayersをループする
  3. 使用teamIdから得TeamsPlayers、呼び出しをGET /Teams/{teamId}返されたデータを保存し、
  4. ループが終了した後。ループに入ったすべてのチームを返します。

同じアルゴリズムを使用して、を呼び出すときにチームからすべてのプレーヤーを取得できます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はシンプルで簡単です。

関係が作成、更新、または削除されるPlayersと、Teamsリソースとリソースの両方が自動的に更新されます。


TeamsPlayersリソースを紹介することは本当に理にかなっています。素晴らしい
ビジェイ

最高の説明
ダイアナ

1

この質問に対して承認されたとマークされた回答があることは知っていますが、以前に発生した問題を解決する方法を次に示します。

PUTとしましょう

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

例として、以下はすべて単一のリソースで行われるため、同期する必要なしに同じ効果が得られます。

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

ここで、1つのチームの複数のメンバーシップを更新したい場合は、次のように(適切な検証を使用して)実行できます。

PUT    /membership/teams/team1/

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

-3
  1. / players(マスターリソースです)
  2. / teams / {id} / players(関係リソースなので、1とは異なる反応をします)
  3. / memberships(関係ですが、意味的に複雑です)
  4. / players / memberships(関係ですが、意味的に複雑です)

私は2を好む


2
おそらく私は答えを理解していないだけかもしれませんが、この投稿は質問に答えていないようです。
BradleyDotNET 2014年

これは質問に対する答えを提供しません。批評したり、著者に説明を要求したりするには、投稿の下にコメントを残します。自分の投稿にはいつでもコメントできます。評判を得ると、どの投稿にもコメントできるようになります。
違法な議論2014年

4
@IllegalArgument It 回答であり、コメントとしては意味がありません。しかし、それは最大の答えではありません。
Qix-モニカは2014年

1
この答えは理解するのが難しく、理由もありません。
Venkat D.16年

2
これでは、質問の説明や回答はまったく行われません。
Manjit Kumar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.