PlayerとTeamの2つのエンティティがあり、プレーヤーが複数のチームに所属できるとします。データモデルには、各エンティティのテーブルと、関係を維持するための結合テーブルがあります。 Hibernateはこれをうまく処理しますが、この関係をRESTful APIで公開するにはどうすればよいですか?
いくつかの方法が考えられます。まず、各エンティティに他のエンティティのリストを含めることができます。したがって、Playerオブジェクトには所属するチームのリストがあり、各Teamオブジェクトには所属するプレイヤーのリストがあります。そのため、プレーヤーをチームに追加するには、プレーヤーの表現をエンドポイントにPOSTするだけです。POST /player
またはPOST /team
のように、リクエストのペイロードとして適切なオブジェクトを使用します。これは私にとって最も「落ち着いた」ように思えますが、少し奇妙に感じます。
/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エンティティのリストを取得します。プレーヤーをチームに追加するには、POST /playerteam
を適切にビルドされたPlayerTeamエンティティをペイロードとして使用します。
/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'
]
これのベストプラクティスは何ですか?
RESTfulインターフェースでは、リソース間の関係をリンクとしてエンコードすることにより、リソース間の関係を説明するドキュメントを返すことができます。したがって、チームは、チームのプレーヤー(/team/{id}/players
)へのリンクのリストであるドキュメントリソース(/player/{id}
)を持っていると言うことができ、プレーヤーは、チームへのリンクのリストであるドキュメントリソース(/player/{id}/teams
)プレイヤーはメンバーです。素敵で対称的。そのリストでマップ操作を簡単に行うことができ、リレーションシップに独自のID(おそらく、チームファーストまたはプレイヤーファーストのリレーションシップを考えているかどうかに応じて、2つのIDが割り当てられます) 。唯一の注意点は、一方の端からリレーションシップを削除する場合、もう一方の端からもリレーションシップを削除することを忘れないでください。ただし、基になるデータモデルを使用し、RESTインターフェイスはそのモデルのビューであるため、それが容易になります。
リレーションシップIDは、チームやプレーヤーに使用するIDの種類に関係なく、UUIDまたは同等の長さでランダムなものに基づいている必要があります。これにより、衝突を心配することなく、リレーションシップの各端のIDコンポーネントと同じUUIDを使用できます(小さな整数はnotに利点があります)。これらのメンバーシップ関係に、プレーヤーとチームを双方向で関連付けるという事実以外のプロパティがある場合、プレーヤーとチームの両方から独立した独自のアイデンティティを持つ必要があります。プレーヤー"チームビュー(/player/{playerID}/teams/{teamID}
)に対するGETは、双方向ビュー(/memberships/{uuid}
)へのHTTPリダイレクトを実行できます。
XLinkxlink:href
属性を使用して、返されるXMLドキュメントにリンクを記述することをお勧めします(もちろんXMLを作成している場合)。
/memberships/
リソースの別個のセットを作成します。
/teams/3/players/
の新しいエンティティを無効にすると、そのリストは無効になりますが、代替URLの/players/5/teams/
をキャッシュに残したくありません。はい、異なるキャッシュには異なるリストの各リストのコピーがあり、それについてできることはあまりありませんが、無効にする必要があるエンティティの数を制限することで、更新をPOSTするユーザーの混乱を少なくとも最小限に抑えることができますクライアントのローカルキャッシュ内の/memberships/98745
のone and one only( Life beyond Distributed Transactions の「代替インデックス」に関するHellandの議論を参照) =より詳細な議論のため)。/players/5/teams
または/teams/3/players
(両方ではない)を選択するだけで、上記の2つのポイントを実装できます。前者を想定しましょう。ただし、ある時点で、currentメンバーシップのリスト用に/players/5/teams/
を予約し、pastメンバーシップはどこかにあります。 /players/5/memberships/
を/memberships/{id}/
リソースへのハイパーリンクのリストにすると、個々のメンバーシップリソースの全員のブックマークを壊さずに、必要に応じて/players/5/past_memberships/
を追加できます。これは一般的な概念です。特定のケースにより当てはまる他の同様の未来を想像できると確信しています。そのような関係をサブリソースとマッピングすると、一般的な設計/トラバースは次のようになります。
# team resource
/teams/{teamId}
# players resource
/players/{playerId}
# teams/players subresource
/teams/{teamId}/players/{playerId}
Restful-termsでは、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を使用します。
既存の回答は、一貫性とi等性の役割を説明しないexplain-UUIDs
の代わりにIDとPUT
のPOST
/乱数の推奨を動機付けます。
「新しいプレーヤーをチームに追加する」のような単純なシナリオがある場合を考えると、一貫性の問題が発生します。
プレーヤーが存在しないため、次のことが必要です。
POST /players { "Name": "Murray" } //=> 302 /players/5
POST /teams/1/players/5
ただし、POST
から/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シーケンスなどの外部データを除外する必要があります。これが、人々がPUT
sに対してUUID
とId
sの両方を一緒に推奨する理由です。
これにより、結果なしで/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がどれだけ柔軟で信頼できるかという全体像のコンテキストにそれらを置くことを望みます。
私が推奨する解決策は、3つのリソースを作成することです:Players
、Teams
、およびTeamsPlayers
。
したがって、チームのすべてのプレーヤーを取得するには、Teams
リソースに移動し、GET /Teams/{teamId}/Players
を呼び出してすべてのプレーヤーを取得します。
一方、プレーヤーがプレイしたすべてのチームを取得するには、Teams
内のPlayers
リソースを取得します。 GET /Players/{playerId}/Teams
を呼び出します。
そして、多対多の関係を取得するには、GET /Players/{playerId}/TeamsPlayers
またはGET /Teams/{teamId}/TeamsPlayers
を呼び出します。
このソリューションでは、GET /Players/{playerId}/Teams
を呼び出すと、Teams
リソースの配列を取得します。これは、GET /Teams/{teamId}
を呼び出したときに取得するリソースとまったく同じです。逆も同様の原理に従い、GET /Teams/{teamId}/Players
を呼び出すとPlayers
リソースの配列を取得します。
どちらの呼び出しでも、関係に関する情報は返されません。たとえば、返されるリソースにはリレーションシップに関する情報がなく、それ自体のリソースに関する情報しかないため、contractStartDate
は返されません。
N-n関係を処理するには、GET /Players/{playerId}/TeamsPlayers
またはGET /Teams/{teamId}/TeamsPlayers
を呼び出します。これらの呼び出しは、リソースTeamsPlayers
を返します。
このTeamsPlayers
リソースには、id
、playerId
、teamId
属性、および関係を記述する他の属性があります。また、それらに対処するために必要なメソッドがあります。関係リソースを返し、組み込み、更新、削除するGET、POST、PUT、DELETEなど。
TeamsPlayers
リソースは、GET /TeamsPlayers?player={playerId}
などのクエリを実装して、{playerId}
で識別されるプレーヤーが持つすべてのTeamsPlayers
関係を返します。同じ考え方に従って、GET /TeamsPlayers?team={teamId}
を使用して、{teamId}
チームでプレイしたTeamsPlayers
をすべて返します。いずれかのGET
呼び出しで、リソースTeamsPlayers
が返されます。関係に関連するすべてのデータが返されます。
GET /Players/{playerId}/Teams
(またはGET /Teams/{teamId}/Players
)を呼び出すと、リソースPlayers
(またはTeams
)はTeamsPlayers
を呼び出して、クエリフィルターを使用して関連するチーム(またはプレーヤー)を返します。
GET /Players/{playerId}/Teams
は次のように機能します。
- すべてを検索TeamsPlayersplayerにid = playerIdがあること。 (
GET /TeamsPlayers?player={playerId}
)- 返されるループTeamsPlayers
- teamsPlayersから取得したteamIdを使用して、
GET /Teams/{teamId}
を呼び出し、返されたデータを保存します- ループが終了した後。ループに入ったすべてのチームを返します。
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
リソースの両方が自動的に更新されます。
この質問に受け入れられたとマークされた回答があることは知っていますが、以前に提起された問題を解決する方法は次のとおりです。
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"
},
...
]
}