web-dev-qa-db-ja.com

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

PlayerTeamの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'        
]

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

256

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を作成している場合)。

111
Donal Fellows

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

  1. RESTとは、進化できるシステムを作ることです。現時点では、特定のプレイヤーが特定のチームに所属していることだけを気にすることができますが、将来のある時点で、あなたはに注釈を付けたいと思うでしょうより多くのデータとの関係:彼らがそのチームにいた期間、そのチームを紹介した人、そのチームにいる間コーチがいた/いた人など.
  2. RESTは効率化のためにキャッシュに依存します。これには、キャッシュの原子性と無効化についてある程度の考慮が必要です。 POST /teams/3/players/の新しいエンティティを無効にすると、そのリストは無効になりますが、代替URLの/players/5/teams/をキャッシュに残したくありません。はい、異なるキャッシュには異なるリストの各リストのコピーがあり、それについてできることはあまりありませんが、無効にする必要があるエンティティの数を制限することで、更新をPOSTするユーザーの混乱を少なくとも最小限に抑えることができますクライアントのローカルキャッシュ内の/memberships/98745one and one onlyLife beyond Distributed Transactions の「代替インデックス」に関するHellandの議論を参照) =より詳細な議論のため)。
  3. /players/5/teamsまたは/teams/3/players(両方ではない)を選択するだけで、上記の2つのポイントを実装できます。前者を想定しましょう。ただし、ある時点で、currentメンバーシップのリスト用に/players/5/teams/を予約し、pastメンバーシップはどこかにあります。 /players/5/memberships//memberships/{id}/リソースへのハイパーリンクのリストにすると、個々のメンバーシップリソースの全員のブックマークを壊さずに、必要に応じて/players/5/past_memberships/を追加できます。これは一般的な概念です。特定のケースにより当てはまる他の同様の未来を想像できると確信しています。
238
fumanchu

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

# 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を使用します。

54
manuel aldana

既存の回答は、一貫性とi等性の役割を説明しないexplain-UUIDsの代わりにIDとPUTPOST /乱数の推奨を動機付けます。

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

プレーヤーが存在しないため、次のことが必要です。

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

これにより、結果なしで/playersPUT/membershipsPUTの両方を再実行できます。

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

すべてが正常であり、部分的な障害に対して再試行する以外に何もする必要はありませんでした。

これは、既存の回答に対する補足です。しかし、ReSTがどれだけ柔軟で信頼できるかという全体像のコンテキストにそれらを置くことを望みます。

16
Seth

私が推奨する解決策は、3つのリソースを作成することです:PlayersTeams、および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リソースには、idplayerIdteamId属性、および関係を記述する他の属性があります。また、それらに対処するために必要なメソッドがあります。関係リソースを返し、組み込み、更新、削除する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は次のように機能します。

  1. すべてを検索TeamsPlayersplayerid = playerIdがあること。 (GET /TeamsPlayers?player={playerId}
  2. 返されるループTeamsPlayers
  3. teamsPlayersから取得したteamIdを使用して、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リソースの両方が自動的に更新されます。

5
Haroldo Macedo

この質問に受け入れられたとマークされた回答があることは知っていますが、以前に提起された問題を解決する方法は次のとおりです。

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"
        },
        ...
    ]
}
1
Heidar Pirzadeh