Làm cách nào để xử lý các mối quan hệ nhiều-nhiều trong API RESTful?


288

Hãy tưởng tượng bạn có 2 thực thể, Người chơiĐội , nơi người chơi có thể ở nhiều đội. Trong mô hình dữ liệu của tôi, tôi có một bảng cho mỗi thực thể và bảng tham gia để duy trì các mối quan hệ. Hibernate xử lý việc này tốt, nhưng làm cách nào tôi có thể phơi bày mối quan hệ này trong API RESTful?

Tôi có thể nghĩ ra một vài cách. Đầu tiên, tôi có thể có mỗi thực thể chứa một danh sách các đối tượng khác, vì vậy một đối tượng Người chơi sẽ có một danh sách các Đội mà nó thuộc về và mỗi đối tượng Đội sẽ có một danh sách Người chơi thuộc về nó. Vì vậy, để thêm Người chơi vào Nhóm, bạn chỉ cần POST đại diện của người chơi vào điểm cuối, giống như POST /playerhoặc POST /teamvới đối tượng thích hợp làm trọng tải của yêu cầu. Điều này có vẻ "RESTful" nhất đối với tôi nhưng cảm thấy hơi kỳ lạ.

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

Một cách khác mà tôi có thể nghĩ để làm điều này sẽ là phơi bày mối quan hệ như một nguồn tài nguyên theo đúng nghĩa của nó. Vì vậy, để xem danh sách tất cả người chơi trong một đội nhất định, bạn có thể thực hiện NHẬN /playerteam/team/{id}hoặc một cái gì đó tương tự và lấy lại danh sách các thực thể PlayerTeam. Để thêm người chơi vào một nhóm, POST /playerteamvới thực thể PlayerTeam được xây dựng phù hợp làm trọng tải.

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

Thực hành tốt nhất cho việc này là gì?

Câu trả lời:


129

Trong giao diện RESTful, bạn có thể trả về các tài liệu mô tả mối quan hệ giữa các tài nguyên bằng cách mã hóa các mối quan hệ đó dưới dạng liên kết. Do đó, một nhóm có thể được cho là có tài nguyên tài liệu ( /team/{id}/players) là danh sách các liên kết đến người chơi ( /player/{id}) trong nhóm và người chơi có thể có tài nguyên tài liệu (/player/{id}/teams) đó là danh sách các liên kết đến các đội mà người chơi là thành viên. Đẹp và đối xứng. Bạn có thể dễ dàng thực hiện các thao tác trên bản đồ trong danh sách đó, thậm chí tạo một mối quan hệ ID riêng của mình (có thể cho rằng họ có hai ID, tùy thuộc vào việc bạn nghĩ về mối quan hệ trước hay người chơi trước) nếu điều đó giúp mọi việc dễ dàng hơn . Điều khó khăn duy nhất là bạn cũng phải nhớ xóa mối quan hệ từ đầu bên kia nếu bạn xóa nó từ một đầu, nhưng xử lý nghiêm ngặt điều này bằng cách sử dụng mô hình dữ liệu cơ bản và sau đó giao diện REST có thể xem mô hình đó sẽ làm cho điều đó dễ dàng hơn.

ID mối quan hệ có lẽ phải dựa trên UUID hoặc một cái gì đó dài và ngẫu nhiên như nhau, bất kể loại ID nào bạn sử dụng cho các đội và người chơi. Điều đó sẽ cho phép bạn sử dụng cùng UUID làm thành phần ID cho mỗi đầu của mối quan hệ mà không phải lo lắng về xung đột (số nguyên nhỏ không có lợi thế đó). Nếu các mối quan hệ thành viên này có bất kỳ tính chất nào khác ngoài thực tế là họ liên quan đến một người chơi và một đội theo kiểu hai chiều, họ nên có bản sắc riêng độc lập với cả người chơi và đội; một GET trên trình phát »chế độ xem nhóm ( /player/{playerID}/teams/{teamID}) sau đó có thể thực hiện chuyển hướng HTTP đến chế độ xem hai chiều ( /memberships/{uuid}).

Tôi khuyên bạn nên viết liên kết trong bất kỳ tài liệu XML nào bạn trả lại (nếu bạn tình cờ sản xuất XML) bằng cách sử dụng các thuộc tính XLink xlink:href .


265

Tạo một bộ /memberships/tài nguyên riêng biệt .

  1. REST là về việc tạo ra các hệ thống có thể phát triển nếu không có gì khác. Tại thời điểm này, bạn chỉ có thể quan tâm rằng một người chơi nhất định thuộc về một đội nhất định, nhưng tại một thời điểm nào đó trong tương lai, bạn sẽ muốn chú thích mối quan hệ đó với nhiều dữ liệu hơn: họ đã ở trong đội đó bao lâu, họ đã giới thiệu họ với đội đó, huấn luyện viên của họ đã / đang ở trong đội đó, v.v.
  2. REST phụ thuộc vào bộ nhớ đệm cho hiệu quả, đòi hỏi một số cân nhắc về tính nguyên tử và tính không hợp lệ của bộ đệm. Nếu bạn POST một thực thể mới vào /teams/3/players/danh sách đó sẽ bị vô hiệu, nhưng bạn không muốn URL thay thế /players/5/teams/vẫn được lưu trữ. Có, các bộ đệm khác nhau sẽ có các bản sao của mỗi danh sách với các độ tuổi khác nhau và chúng tôi không thể làm gì nhiều về điều đó, nhưng ít nhất chúng tôi có thể giảm thiểu sự nhầm lẫn cho người dùng BÀI ĐĂNG cập nhật bằng cách giới hạn số lượng thực thể chúng tôi cần để vô hiệu hóa trong bộ nhớ cache cục bộ của khách hàng của họ cho một và chỉ một người tại /memberships/98745(xem phần thảo luận của Helland về "chỉ số thay thế" trong Cuộc sống ngoài Giao dịch phân tán để thảo luận chi tiết hơn).
  3. Bạn có thể thực hiện 2 điểm trên chỉ bằng cách chọn /players/5/teamshoặc /teams/3/players(nhưng không phải cả hai). Hãy giả sử trước đây. Tuy nhiên, tại một số điểm, bạn sẽ muốn dành /players/5/teams/cho một danh sách các thành viên hiện tại và vẫn có thể đề cập đến các thành viên trong quá khứ ở đâu đó. Tạo /players/5/memberships/một danh sách các siêu liên kết đến /memberships/{id}/các tài nguyên và sau đó bạn có thể thêm /players/5/past_memberships/khi bạn muốn mà không phải phá vỡ dấu trang của mọi người cho các tài nguyên thành viên riêng lẻ. Đây là một khái niệm chung; Tôi chắc chắn bạn có thể tưởng tượng các tương lai tương tự khác phù hợp hơn với trường hợp cụ thể của bạn.

11
Điểm 1 & 2 được giải thích hoàn hảo, cảm ơn, nếu ai có nhiều thịt hơn cho điểm 3 trong trải nghiệm thực tế, điều đó sẽ giúp tôi.
Alain

2
IMO trả lời tốt nhất và đơn giản nhất! Có hai điểm cuối và giữ chúng đồng bộ có một loạt các biến chứng.
Venkat D.

7
chào fumanchu. Câu hỏi: Trong phần còn lại / thành viên / 98745, con số đó ở cuối url thể hiện điều gì? Đây có phải là một id duy nhất cho các thành viên? Làm thế nào một người sẽ tương tác với các điểm cuối thành viên? Để thêm người chơi, POST sẽ được gửi có chứa trọng tải với {team: 3, player: 6}, từ đó tạo liên kết giữa hai người? Điều gì về một GET? bạn có thể gửi một GET tới / thành viên không? player = và / thành viên? đội = để có kết quả? Đó là ý tưởng? Tôi có thiếu thứ gì không? (Tôi đang cố gắng học các điểm cuối yên tĩnh) Trong trường hợp đó, id 98745 trong tư cách thành viên / 98745 có thực sự hữu ích không?
aruuuuu

@aruuuuu một điểm cuối riêng cho một hiệp hội nên được cung cấp với PK thay thế. Nó làm cho cuộc sống dễ dàng hơn rất nhiều nói chung: / thành viên / {thành viên}. Khóa (playerId, teamId) vẫn là duy nhất và do đó có thể được sử dụng trên các tài nguyên có mối quan hệ này: / Đội / {teamId} / người chơi và / người chơi / {playerId} / đội. Nhưng không phải lúc nào cũng được duy trì khi các mối quan hệ như vậy được duy trì ở cả hai phía. Ví dụ: Bí quyết và Thành phần: bạn sẽ hầu như không cần sử dụng / thành phần / {thành phần} / công thức nấu ăn /.
Alexander Palamarchuk

65

Tôi sẽ ánh xạ mối quan hệ như vậy với các tài nguyên phụ, thiết kế chung / truyền tải sau đó sẽ là:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

Trong Restful-terms, nó giúp ích rất nhiều cho việc không nghĩ về SQL và tham gia mà nhiều hơn vào các bộ sưu tập, các bộ sưu tập phụ và truyền tải.

Vài ví dụ:

# 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

Như bạn thấy, tôi không sử dụng POST để đặt người chơi vào các đội nhưng PUT, điều này xử lý mối quan hệ giữa người chơi và đội của bạn tốt hơn.


20
Điều gì xảy ra nếu team_player có thêm thông tin như trạng thái, v.v.? chúng tôi đại diện cho nó trong mô hình của bạn ở đâu? chúng ta có thể quảng cáo tài nguyên đó và cung cấp URL cho tài nguyên đó không, giống như trò chơi /, người chơi /
Narendra Kamma

Xin chào, câu hỏi nhanh chỉ để đảm bảo rằng tôi hiểu đúng: NHẬN / đội / 1 / người chơi / 3 trả về một nội dung phản hồi trống. Phản hồi có ý nghĩa duy nhất từ ​​đây là 200 so với 404. Thông tin của thực thể người chơi (tên, tuổi, v.v.) KHÔNG được trả về bởi GET / đội / 1 / người chơi / 3. Nếu khách hàng muốn nhận thêm thông tin về người chơi, anh ta phải NHẬN / người chơi / 3. Đây có phải là tất cả chính xác?
Verdagon

2
Tôi đồng ý với bản đồ của bạn, nhưng có một câu hỏi. Đó là vấn đề quan điểm cá nhân, nhưng bạn nghĩ gì về POST / đội / 1 / người chơi và tại sao bạn không sử dụng nó? Bạn có thấy bất kỳ nhược điểm / sai lệch trong phương pháp này?
JakubKnejzlik

2
POST không phải là idempotent, tức là nếu bạn thực hiện POST / đội / 1 / người chơi n lần, bạn sẽ thay đổi n lần / đội / 1. nhưng việc chuyển người chơi sang / đội / 1 lần sẽ không thay đổi trạng thái của đội nên việc sử dụng PUT là rõ ràng hơn.
manuel aldana

1
@NarendraKamma Tôi đoán chỉ cần gửi statusdưới dạng param trong yêu cầu PUT? Có một nhược điểm của phương pháp đó?
Traxo

22

Các câu trả lời hiện tại không giải thích vai trò của tính nhất quán và tính không thay đổi - điều này thúc đẩy các khuyến nghị của họ về UUIDs/ số ngẫu nhiên cho ID và PUTthay vì POST.

Nếu chúng ta xem xét trường hợp chúng ta có một kịch bản đơn giản như " Thêm người chơi mới vào một đội ", chúng ta sẽ gặp phải các vấn đề về tính nhất quán.

Vì người chơi không tồn tại, chúng tôi cần:

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

Tuy nhiên, hoạt động của khách hàng nên thất bại sau khi POSTđể /players, chúng tôi đã tạo một cầu thủ mà không thực hiện thuộc về một nhóm:

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

Bây giờ chúng tôi có một người chơi trùng lặp mồ côi trong /players/5.

Để khắc phục điều này, chúng tôi có thể viết mã khôi phục tùy chỉnh để kiểm tra những người chơi mồ côi phù hợp với một số khóa tự nhiên (ví dụ Name). Đây là mã tùy chỉnh cần được kiểm tra, tốn nhiều tiền và thời gian hơn, v.v.

Để tránh cần mã khôi phục tùy chỉnh, chúng tôi có thể thực hiện PUTthay vì POST.

Từ RFC :

ý định của PUTidempotent

Để một hoạt động là idempotent, nó cần loại trừ dữ liệu ngoài như chuỗi id do máy chủ tạo. Đây là lý do tại sao mọi người đề xuất cả hai PUTUUIDs cho Ids cùng nhau.

Điều này cho phép chúng tôi chạy lại cả /players PUT/memberships PUTkhông có hậu quả:

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

Mọi thứ đều ổn và chúng tôi không cần phải làm gì hơn là thử lại cho những thất bại một phần.

Đây là phần bổ sung cho các câu trả lời hiện có nhưng tôi hy vọng nó đặt chúng trong bối cảnh của bức tranh lớn hơn về việc ReST linh hoạt và đáng tin cậy như thế nào.


Trong điểm cuối giả thuyết này, bạn đã lấy 23lkrjrqwlejtừ đâu?
cbcoutinho

1
mặt cuộn trên bàn phím - không có gì đặc biệt về 23lkr ... gobbledegook khác hơn là nó không tuần tự hoặc có ý nghĩa
Seth

9

Giải pháp của tôi ưa thích là để tạo ra ba nguồn: Players, TeamsTeamsPlayers.

Vì vậy, để có được tất cả các cầu thủ của một đội, chỉ cần truy cập Teamstài nguyên và nhận tất cả các cầu thủ của đội bằng cách gọi GET /Teams/{teamId}/Players.

Mặt khác, để có được tất cả các đội mà một người chơi đã chơi, hãy lấy Teamstài nguyên trong Players. Gọi GET /Players/{playerId}/Teams.

Và, để có được cuộc gọi nhiều mối quan hệ GET /Players/{playerId}/TeamsPlayershay GET /Teams/{teamId}/TeamsPlayers.

Lưu ý rằng, trong giải pháp này, khi bạn gọi GET /Players/{playerId}/Teams, bạn nhận được một mảng Teamstài nguyên, đó chính xác là tài nguyên bạn nhận được khi bạn gọi GET /Teams/{teamId}. Ngược lại theo cùng một nguyên tắc, bạn nhận được một loạt các Playerstài nguyên khi gọiGET /Teams/{teamId}/Players .

Trong cả hai cuộc gọi, không có thông tin nào về mối quan hệ được trả lại. Ví dụ, không contractStartDateđược trả về, vì tài nguyên được trả về không có thông tin về mối quan hệ, chỉ về tài nguyên của chính nó.

Để đối phó với các mối quan hệ nn, gọi một trong hai GET /Players/{playerId}/TeamsPlayershoặc GET /Teams/{teamId}/TeamsPlayers. Các cuộc gọi này trả về tài nguyên chính xác TeamsPlayers,.

Đây TeamsPlayersnguồn có id, playerId,teamId thuộc tính, cũng như một số người khác để mô tả mối quan hệ. Ngoài ra, nó có các phương pháp cần thiết để đối phó với chúng. NHẬN, POST, PUT, DELETE, vv sẽ trả về, bao gồm, cập nhật, xóa tài nguyên mối quan hệ.

Tài TeamsPlayersnguyên thực hiện một số truy vấn, như GET /TeamsPlayers?player={playerId}trả về tất cả các TeamsPlayersmối quan hệ mà người chơi đã xác định bằng {playerId}. Theo cùng một ý tưởng, sử dụng GET /TeamsPlayers?team={teamId}để trả lại tất cả những TeamsPlayersgì đã chơi trong {teamId}đội. Trong cả hai GETcuộc gọi, tài nguyên TeamsPlayersđược trả về. Tất cả các dữ liệu liên quan đến mối quan hệ được trả lại.

Khi gọi GET /Players/{playerId}/Teams(hoặc GET /Teams/{teamId}/Players), tài nguyên Players(hoặc Teams) gọi TeamsPlayersđể trả về các nhóm (hoặc người chơi) có liên quan bằng bộ lọc truy vấn.

GET /Players/{playerId}/Teams hoạt động như thế này:

  1. Tìm tất cả các TeamsPlayers mà người chơiid = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Lặp lại các TeamsPlayers đã trả lại
  3. Sử dụng teamId thu được từ TeamsPlayers , gọi GET /Teams/{teamId}và lưu trữ dữ liệu trả về
  4. Sau khi vòng lặp kết thúc. Trả lại tất cả các đội đã có trong vòng lặp.

Bạn có thể sử dụng cùng một thuật toán để nhận tất cả người chơi từ một đội, khi gọi GET /Teams/{teamId}/Players, nhưng trao đổi đội và người chơi.

Tài nguyên của tôi sẽ như thế này:

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

Giải pháp này chỉ dựa vào tài nguyên REST. Mặc dù một số cuộc gọi bổ sung có thể cần thiết để lấy dữ liệu từ người chơi, đội hoặc mối quan hệ của họ, tất cả các phương thức HTTP đều được thực hiện dễ dàng. POST, PUT, DELETE rất đơn giản và dễ hiểu.

Bất cứ khi nào một mối quan hệ được tạo, cập nhật hoặc xóa, cả hai PlayersTeamstài nguyên đều được cập nhật tự động.


thật sự có ý nghĩa khi giới thiệu tài nguyên TeamsPlayers.Awgie
vijay

giải thích tốt nhất
Diana

1

Tôi biết rằng có một câu trả lời được đánh dấu là chấp nhận cho câu hỏi này, tuy nhiên, đây là cách chúng tôi có thể giải quyết các vấn đề được nêu ra trước đây:

Hãy nói cho PUT

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

Ví dụ, tất cả các phần sau sẽ cho kết quả tương tự mà không cần đồng bộ hóa vì chúng được thực hiện trên một tài nguyên duy nhất:

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

bây giờ nếu chúng tôi muốn cập nhật nhiều thành viên cho một nhóm, chúng tôi có thể làm như sau (với các xác nhận hợp lệ):

PUT    /membership/teams/team1/

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

-3
  1. / người chơi (là tài nguyên chính)
  2. / đội / {id} / người chơi (là tài nguyên mối quan hệ, do đó, nó phản ứng khác nhau 1)
  3. / thành viên (là một mối quan hệ nhưng phức tạp về mặt ngữ nghĩa)
  4. / người chơi / thành viên (là một mối quan hệ nhưng phức tạp về mặt ngữ nghĩa)

Tôi thích 2


2
Có lẽ tôi chỉ không hiểu câu trả lời, nhưng bài này dường như không trả lời được câu hỏi.
BradleyDotNET

Điều này không cung cấp một câu trả lời cho câu hỏi. Để phê bình hoặc yêu cầu làm rõ từ một tác giả, hãy để lại nhận xét bên dưới bài đăng của họ - bạn luôn có thể nhận xét về bài đăng của riêng bạn và khi bạn có đủ danh tiếng, bạn sẽ có thể nhận xét về bất kỳ bài đăng nào .
Luận cứ bất hợp pháp

4
@IllegalArgument Đó một câu trả lời và sẽ không có ý nghĩa như một nhận xét. Tuy nhiên, đó không phải là câu trả lời lớn nhất.
Qix - MONICA ĐƯỢC PHÂN PHỐI

1
Câu trả lời này rất khó để làm theo và không cung cấp lý do.
Venkat D.

2
Điều này không giải thích hoặc trả lời câu hỏi nào cả.
Manjit Kumar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.