RESTful API에서 다 대다 관계를 처리하는 방법은 무엇입니까?


288

플레이어가 여러 팀에있을 수있는 두 개의 엔티티 ( PlayerTeam) 가 있다고 가정하십시오 . 내 데이터 모델에는 각 엔터티에 대한 테이블과 관계를 유지하기위한 조인 테이블이 있습니다. Hibernate는 이것을 잘 처리하지만 RESTful API에서 어떻게이 관계를 노출시킬 수 있습니까?

몇 가지 방법을 생각할 수 있습니다. 먼저 각 엔터티에 다른 엔터티의 목록이 포함되어있을 수 있으므로 Player 객체에는 소속 된 팀 목록이 있고 각 Team 객체에는 소속 된 플레이어 목록이 있습니다. 따라서 플레이어를 팀에 추가하려면 플레이어의 표현을 요청의 페이로드로 적절한 객체를 사용하여 POST /player또는 POST /team와 같은 엔드 포인트에 POST 하면됩니다. 이것은 나에게 가장 "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'
}

내가 이것을 생각할 수있는 다른 방법은 관계를 그 자체로 리소스로 노출시키는 것입니다. 따라서 주어진 팀의 모든 플레이어 목록을 보려면 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를 줄 수도 있습니다 (아마도 팀 우선 또는 플레이어 우선 관계에 대해 생각하고 있는지에 따라 두 개의 ID를 가질 것입니다) . 유일한 까다로운 점은 한쪽 끝에서 관계를 삭제하면 다른 쪽 끝에서도 관계를 삭제하는 것을 기억해야하지만 기본 데이터 모델을 사용하여 엄격하게 처리 한 다음 REST 인터페이스를 그 모델이 더 쉬워 질 것입니다.

관계 ID는 팀 및 플레이어에 사용하는 ID 유형에 관계없이 UUID 또는 동일하고 길고 임의의 항목을 기반으로해야합니다. 즉 (작은 정수는 않습니다 당신이 충돌에 대한 걱정없이 관계의 각 끝의 ID 구성 요소와 동일한 UUID를 사용하게됩니다 하지 그 장점이 있습니다). 이러한 멤버십 관계에 양방향 방식으로 선수와 팀을 관련 시킨다는 사실 이외의 다른 속성이있는 경우, 선수와 팀 모두와 독립적 인 고유 한 정체성을 가져야합니다. 플레이어»팀보기 ( /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하는 사용자의 혼동을 최소한 최소화 할 수 있습니다. 그들의 클라이언트의 로컬 캐시에 오직 하나/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에 더 많은 고기를 가지고 있다면 도움이 될 것입니다.
Alain

2
가장 간단한 답변 IMO 감사합니다! 두 개의 엔드 포인트가 있고 동기화 상태를 유지하면 많은 문제가 발생합니다.
Venkat D.

7
안녕 후 만추. 질문 : 나머지 엔드 포인트 / memberships / 98745에서 URL 끝에있는 숫자는 무엇을 나타 냅니까? 회원의 고유 ID입니까? 멤버쉽 엔드 포인트와 어떻게 상호 작용합니까? 플레이어를 추가하기 위해 {team : 3, player : 6}으로 페이로드가 포함 된 POST를 보내면 둘 사이에 링크가 생성됩니까? GET은 어떻습니까? 결과를 얻으려면 / memberships? player = 및 / membersihps? team =에 GET을 보내시겠습니까? 그게 아이디어야? 아무것도 빠졌습니까? (나는 편안한 종점을 배우려고 노력하고있다) 그 경우, 회원 번호 / 98745의 ID 98745가 실제로 유용합니까?
aruuuuu

@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}

편안한 용어로 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

2
귀하의지도에 동의하지만 한 가지 질문이 있습니다. 개인적인 의견의 문제이지만 POST / team / 1 / players에 대해 어떻게 생각하고 사용하지 않습니까? 이 접근 방식에 단점 / 오해의 소지가 있습니까?
JakubKnejzlik

2
POST는 dem 등성이 아닙니다. 즉, POST / teams / 1 / players n-times를 수행하면 n-times / teams / 1을 변경하게됩니다. 그러나 플레이어를 / teams / 1 n 번으로 이동해도 팀의 상태가 변경되지 않으므로 PUT을 사용하는 것이 더 분명합니다.
manuel aldana

1
@NarendraKamma statusPUT 요청에서 매개 변수로 보내겠다고 생각 합니까? 그 접근법에 단점이 있습니까?
Traxo

22

기존 답변 에는 일관성 및 dem 등원의 역할이 설명 되어 있지 않습니다. 이는 UUIDsID PUT대신 / 랜덤 번호에 대한 권장 사항을 동기 부여합니다 POST.

" 팀에 새 선수 추가 "와 같은 간단한 시나리오가있는 경우 일관성 문제가 발생합니다.

플레이어가 존재하지 않기 때문에 다음을 수행해야합니다.

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는 dem 등이다

조작이 dem 등원 (idempotent)이 되려면 서버에서 생성 된 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의 특별한 점은 순차적이거나 의미가 없습니다.
Seth

9

나의 선호하는 솔루션은 세 가지 리소스를 생성하는 것입니다 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}. 그 반대의 경우도 마찬가지 입니다. Playerscall시 여러 리소스 를 얻습니다 GET /Teams/{teamId}/Players.

두 호출에서 관계에 대한 정보가 리턴되지 않습니다. 예를 들어, contractStartDate리턴 된 자원에 관계에 대한 정보가없고 자체 자원에 대한 정보 만 있기 때문에 no 가 리턴됩니다.

nn 관계를 처리하려면 GET /Players/{playerId}/TeamsPlayers또는을 호출하십시오 GET /Teams/{teamId}/TeamsPlayers. 이 호출은 정확히 리소스를 반환합니다 TeamsPlayers.

TeamsPlayers자원이있다 id, playerId, teamId속성뿐만 아니라 일부 다른 관계를 설명합니다. 또한이를 처리하는 데 필요한 방법이 있습니다. 관계 자원을 리턴, 포함, 갱신 및 제거 할 GET, POST, PUT, DELETE 등.

TeamsPlayers자원 구현 일부 쿼리는 원하는 GET /TeamsPlayers?player={playerId}모든 반환 TeamsPlayers에 의해 확인 된 플레이어는 관계 {playerId}가 있습니다. 같은 생각 에 따라 팀 에서 플레이 한 GET /TeamsPlayers?team={teamId}모든 것을 반환하십시오 . 어느 호출 에서나 리소스 가 반환됩니다. 관계와 관련된 모든 데이터가 반환됩니다.TeamsPlayers{teamId}GETTeamsPlayers

GET /Players/{playerId}/Teams(또는 GET /Teams/{teamId}/Players)을 호출 할 때 리소스 Players(또는 Teams) TeamsPlayers는 쿼리 필터를 사용하여 관련 팀 (또는 플레이어)을 반환하기 위해 호출 합니다.

GET /Players/{playerId}/Teams 다음과 같이 작동합니다.

  1. 플레이어id = playerId 인 모든 TeamsPlayer 를 찾으십시오 . ( )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는 간단하고 간단합니다.

관계가 작성, 업데이트 또는 삭제 될 때마다 PlayersTeams자원이 모두 자동으로 업데이트됩니다.


TeamsPlayers 리소스를 소개하는 것이 좋습니다. 멋진
vijay

최고의 설명
Diana

1

이 질문에 허용되는 것으로 표시되어 있지만 이전에 제기 된 문제를 해결하는 방법은 다음과 같습니다.

PUT에 대해 말해 봅시다

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

예를 들어, 다음은 단일 리소스에서 수행되므로 동기화 할 필요없이 동일한 결과를 가져옵니다.

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

이제 한 팀의 여러 멤버십을 업데이트하려는 경우 다음과 같이 수행 할 수 있습니다 (적절한 검증).

PUT    /membership/teams/team1/

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

-3
  1. / players (마스터 리소스)
  2. / teams / {id} / players (관계 리소스이므로 1과 다른 반응을 보입니다)
  3. / 멤버쉽 (관계는 있지만 의미 적으로 복잡합니다)
  4. / players / memberships (관계는 있지만 의미 상 복잡합니다)

나는 2를 선호한다


2
아마도 나는 대답을 이해하지 못하지만이 게시물은 질문에 대답하지 않는 것 같습니다.
BradleyDotNET 2016 년

이것은 질문에 대한 답변을 제공하지 않습니다. 작성자의 의견을 비판하거나 설명을 요청하려면 게시물 아래에 댓글을 남겨주세요. 언제든지 자신의 게시물 에 댓글 수 있으며 평판 이 충분 하면 게시물댓글 수 있습니다 .
불법 인수

4
@IllegalArgument 그것은 이다 응답 및 주석으로 이해가되지 것입니다. 그러나 가장 큰 답은 아닙니다.
Qix-MONICA가

1
이 답변은 따르기가 어렵고 이유를 제공하지 않습니다.
Venkat D.

2
이것은 질문에 대한 설명이나 대답이 아닙니다.
Manjit Kumar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.