Bagaimana menangani hubungan banyak ke banyak dalam API yang tenang?

288

Bayangkan Anda memiliki 2 entitas, Pemain dan Tim , di mana pemain dapat berada di beberapa tim. Dalam model data saya, saya memiliki tabel untuk setiap entitas, dan tabel gabungan untuk mempertahankan hubungan. Hibernate baik-baik saja dalam menangani hal ini, tetapi bagaimana saya bisa mengekspos hubungan ini dalam RESTful API?

Saya bisa memikirkan beberapa cara. Pertama, saya mungkin meminta setiap entitas berisi daftar yang lain, jadi objek Player akan memiliki daftar Tim miliknya, dan setiap objek Tim akan memiliki daftar Pemain yang menjadi miliknya. Jadi untuk menambahkan Pemain ke Tim, Anda hanya akan POST representasi pemain ke titik akhir, sesuatu seperti POST /playeratau POST /teamdengan objek yang sesuai sebagai muatan permintaan. Ini tampaknya yang paling "tenang" bagi saya tetapi terasa agak aneh.

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

Cara lain yang bisa saya pikirkan untuk melakukan ini adalah dengan mengekspos hubungan sebagai sumber daya dalam dirinya sendiri. Jadi untuk melihat daftar semua pemain di tim tertentu, Anda bisa melakukan GET /playerteam/team/{id}atau sesuatu seperti itu dan mendapatkan kembali daftar entitas PlayerTeam. Untuk menambahkan pemain ke tim, POST /playerteamdengan entitas PlayerTeam yang dibangun dengan tepat sebagai muatan.

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

Apa praktik terbaik untuk ini?

Richard Handworker
sumber

Jawaban:

129

Di antarmuka RESTful, Anda bisa mengembalikan dokumen yang menggambarkan hubungan antara sumber daya dengan menyandikan hubungan tersebut sebagai tautan. Dengan demikian, sebuah tim dapat dikatakan memiliki sumber dokumen ( /team/{id}/players) yang merupakan daftar tautan ke pemain ( /player/{id}) di tim, dan pemain dapat memiliki sumber dokumen (/player/{id}/teams) itu adalah daftar tautan ke tim yang menjadi anggota pemain. Bagus dan simetris. Anda dapat memetakan operasi pada daftar itu dengan cukup mudah, bahkan memberikan hubungan ID-nya sendiri (bisa dibilang mereka memiliki dua ID, tergantung pada apakah Anda memikirkan tim hubungan-pertama atau pemain-pertama) jika itu membuat segalanya lebih mudah . Satu-satunya hal yang sulit adalah Anda harus ingat untuk menghapus hubungan dari ujung yang lain juga jika Anda menghapusnya dari satu sisi, tetapi dengan keras menangani ini dengan menggunakan model data yang mendasarinya dan kemudian memiliki antarmuka REST menjadi pemandangan model itu akan membuatnya lebih mudah.

ID hubungan mungkin harus didasarkan pada UUID atau sesuatu yang sama-sama panjang dan acak, terlepas dari jenis ID apa pun yang Anda gunakan untuk tim dan pemain. Itu akan memungkinkan Anda menggunakan UUID yang sama sebagai komponen ID untuk setiap akhir hubungan tanpa khawatir tentang tabrakan (bilangan bulat kecil tidak memiliki keunggulan itu). Jika hubungan keanggotaan ini memiliki sifat selain fakta telanjang bahwa mereka menghubungkan seorang pemain dan tim dengan cara dua arah, mereka harus memiliki identitas mereka sendiri yang tidak tergantung pada pemain dan tim; GET pada tampilan tim »pemain ( /player/{playerID}/teams/{teamID}) kemudian dapat melakukan pengalihan HTTP ke tampilan dua arah ( /memberships/{uuid}).

Saya sarankan menulis tautan dalam dokumen XML apa pun yang Anda kembalikan (jika Anda menghasilkan XML tentu saja) menggunakan atribut XLink xlink:href .

Donal Fellows
sumber
265

Buat /memberships/sumber daya yang terpisah .

  1. REST adalah tentang membuat sistem yang dapat dikembangkan jika tidak ada yang lain. Pada saat ini, Anda mungkin hanya peduli bahwa pemain tertentu ada di tim tertentu, tetapi di beberapa titik di masa depan, Anda akan ingin menjelaskan hubungan itu dengan lebih banyak data: berapa lama mereka berada di tim itu, yang merujuk mereka ke tim itu, siapa pelatih mereka saat berada di tim itu, dll.
  2. REST bergantung pada caching untuk efisiensi, yang memerlukan beberapa pertimbangan untuk atomicity dan pembatalan cache. Jika Anda POST entitas baru ke /teams/3/players/daftar itu akan tidak valid, tetapi Anda tidak ingin URL alternatif /players/5/teams/tetap di-cache. Ya, cache yang berbeda akan memiliki salinan dari setiap daftar dengan usia yang berbeda, dan tidak banyak yang dapat kita lakukan tentang itu, tetapi kita setidaknya dapat meminimalkan kebingungan bagi pengguna POST'ing pembaruan dengan membatasi jumlah entitas yang perlu kita batalkan dalam cache lokal klien mereka ke satu dan hanya satu di /memberships/98745(lihat diskusi Helland tentang "indeks alternatif" dalam Kehidupan di luar Transaksi Terdistribusi untuk diskusi lebih rinci).
  3. Anda dapat menerapkan 2 poin di atas hanya dengan memilih /players/5/teamsatau /teams/3/players(tetapi tidak keduanya). Mari kita asumsikan yang pertama. Namun, pada titik tertentu, Anda ingin memesan /players/5/teams/daftar keanggotaan saat ini , namun dapat merujuk ke keanggotaan sebelumnya di suatu tempat. Buat /players/5/memberships/daftar hyperlink ke /memberships/{id}/sumber daya, dan kemudian Anda dapat menambahkan /players/5/past_memberships/kapan saja, tanpa harus merusak bookmark semua orang untuk sumber daya keanggotaan individu. Ini adalah konsep umum; Saya yakin Anda bisa membayangkan masa depan serupa lainnya yang lebih berlaku untuk kasus spesifik Anda.
fumanchu
sumber
11
Poin 1 & 2 dijelaskan dengan sempurna, terima kasih, jika ada yang punya lebih banyak daging untuk poin 3 dalam pengalaman kehidupan nyata, itu akan membantu saya.
Alain
2
IMO jawaban terbaik dan paling sederhana terima kasih! Memiliki dua titik akhir dan menjaganya tetap selaras memiliki sejumlah komplikasi.
Venkat D.
7
hai fumanchu. Pertanyaan: Di titik akhir sisanya / keanggotaan / 98745 apa yang dilambangkan angka pada akhir url? Apakah ini id unik untuk keanggotaan? Bagaimana orang berinteraksi dengan titik akhir keanggotaan? Untuk menambahkan pemain, akankah POST dikirimkan berisi muatan dengan {team: 3, player: 6}, sehingga menciptakan tautan di antara keduanya? Bagaimana dengan GET? apakah Anda akan mengirim GET ke / keanggotaan? pemain = dan / membersihps? tim = untuk mendapatkan hasil? Itulah idenya? Apakah saya kehilangan sesuatu? (Saya mencoba mempelajari titik akhir yang tenang) Dalam hal ini, apakah id 98745 dalam keanggotaan / 98745 pernah benar-benar berguna?
aruuuuu
@aruuuuu, titik akhir terpisah untuk sebuah asosiasi harus dilengkapi dengan PK pengganti. Itu membuat hidup jauh lebih mudah juga secara umum: / keanggotaan / {membershipId}. Kuncinya (playerId, teamId) tetap unik dan dengan demikian dapat digunakan pada sumber daya yang memiliki hubungan ini: / teams / {teamId} / pemain dan / pemain / {playerId} / tim. Tapi itu tidak selalu ketika hubungan seperti itu dipertahankan di kedua sisi. Sebagai contoh, Resep dan Bahan: Anda hampir tidak perlu menggunakan / ingredients / {ingredientId} / resep /.
Alexander Palamarchuk
65

Saya akan memetakan hubungan tersebut dengan sub-sumber daya, desain / traversal umum akan menjadi:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

Dalam istilah Restful ini banyak membantu dalam tidak memikirkan SQL dan bergabung tetapi lebih ke koleksi, sub-koleksi dan traversal.

Beberapa contoh:

# 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

Seperti yang Anda lihat, saya tidak menggunakan POST untuk menempatkan pemain ke tim, tetapi PUT, yang menangani hubungan pemain dan tim Anda dengan lebih baik.

manuel aldana
sumber
20
Bagaimana jika team_player memiliki informasi tambahan seperti status dll? di mana kami mewakilinya dalam model Anda? dapatkah kami mempromosikannya ke sumber daya, dan memberikan URL untuknya, seperti halnya game /, pemain /
Narendra Kamma
Hei, pertanyaan singkat hanya untuk memastikan saya mendapatkan ini dengan benar: GET / tim / 1 / pemain / 3 mengembalikan badan tanggapan yang kosong. Satu-satunya respons yang berarti dari ini adalah 200 vs 404. Informasi entitas pemain (nama, usia, dll) TIDAK dikembalikan oleh GET / tim / 1 / pemain / 3. Jika klien ingin mendapatkan informasi tambahan tentang pemain, ia harus MENDAPATKAN / pemain / 3. Apakah ini benar?
Verdagon
2
Saya setuju dengan pemetaan Anda, tetapi ada satu pertanyaan. Ini masalah pendapat pribadi, tetapi apa pendapat Anda tentang POST / tim / 1 / pemain dan mengapa Anda tidak menggunakannya? Apakah Anda melihat ada kerugian / menyesatkan dalam pendekatan ini?
JakubKnejzlik
2
POST tidak idempoten, yaitu jika Anda melakukan POST / tim / 1 / pemain n-kali, Anda akan mengubah n-kali / tim / 1. tetapi memindahkan pemain ke / tim / 1 n-kali tidak akan mengubah status tim sehingga menggunakan PUT lebih jelas.
manuel aldana
1
@NarendraKamma Saya kira baru saja mengirim statussebagai param dalam permintaan PUT? Apakah ada kelemahan dari pendekatan itu?
Traxo
22

Jawaban yang ada tidak menjelaskan peran konsistensi dan idempotensi - yang memotivasi rekomendasi UUIDs/ nomor acak mereka untuk ID dan PUTbukan POST.

Jika kami mempertimbangkan kasus di mana kami memiliki skenario sederhana seperti " Tambahkan pemain baru ke tim ", kami mengalami masalah konsistensi.

Karena pemain tidak ada, kita perlu:

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

Namun, jika operasi klien gagal setelah POSTke /players, kami telah membuat pemain yang bukan milik tim:

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

Sekarang kami memiliki duplikat pemain yatim piatu di /players/5.

Untuk memperbaikinya, kami dapat menulis kode pemulihan khusus yang memeriksa pemain yatim yang cocok dengan beberapa kunci alami (mis Name.). Ini adalah kode khusus yang perlu diuji, membutuhkan lebih banyak uang dan waktu, dll

Untuk menghindari perlu kode pemulihan kustom, kita dapat menerapkan PUTbukannya POST.

Dari RFC :

niat PUTidempoten

Agar suatu operasi menjadi idempoten, ia harus mengecualikan data eksternal seperti sekuen id yang dihasilkan server. Inilah sebabnya mengapa orang merekomendasikan keduanya PUTdan UUIDuntuk Idbersama-sama.

Hal ini memungkinkan kami untuk menjalankan kembali baik /players PUTyang /memberships PUTtanpa maupun yang konsekuensi:

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

Semuanya baik-baik saja dan kami tidak perlu melakukan apa pun selain mencoba lagi untuk kegagalan parsial.

Ini lebih merupakan tambahan untuk jawaban yang ada tapi saya harap itu menempatkan mereka dalam konteks gambaran yang lebih besar tentang seberapa fleksibel dan dapat diandalkannya ReST.

Seth
sumber
Dalam titik akhir hipotetis ini, dari mana Anda mendapatkan 23lkrjrqwlej?
cbcoutinho
1
roll face on keyboard - tidak ada yang istimewa tentang 23lkr ... gobbledegook selain itu tidak berurutan atau bermakna
Seth
9

Solusi saya lebih suka adalah untuk membuat tiga sumber: Players, Teamsdan TeamsPlayers.

Jadi, untuk mendapatkan semua pemain tim, pergi saja ke Teamssumber daya dan dapatkan semua pemainnya dengan menelepon GET /Teams/{teamId}/Players.

Di sisi lain, untuk mendapatkan semua tim yang dimainkan pemain, dapatkan Teamssumber daya di dalamnya Players. Panggil GET /Players/{playerId}/Teams.

Dan, untuk mendapatkan panggilan hubungan many-to-many GET /Players/{playerId}/TeamsPlayersatau GET /Teams/{teamId}/TeamsPlayers.

Perhatikan bahwa, dalam solusi ini, saat Anda menelepon GET /Players/{playerId}/Teams, Anda mendapatkan berbagai Teamssumber, yang persis sama dengan yang Anda dapatkan saat menelepon GET /Teams/{teamId}. Kebalikannya mengikuti prinsip yang sama, Anda mendapatkan berbagai Playerssumber daya saat panggilan GET /Teams/{teamId}/Players.

Di salah satu panggilan, tidak ada informasi tentang hubungan yang dikembalikan. Misalnya, tidak ada contractStartDateyang dikembalikan, karena sumber daya yang dikembalikan tidak memiliki info tentang hubungan, hanya tentang sumber dayanya sendiri.

Untuk menangani hubungan nn, hubungi salah satu GET /Players/{playerId}/TeamsPlayersatau GET /Teams/{teamId}/TeamsPlayers. Panggilan ini mengembalikan sumber daya yang tepat TeamsPlayers,.

Ini TeamsPlayerssumber daya telah id, playerId, teamIdatribut, serta beberapa orang lain untuk menggambarkan hubungan. Juga, ia memiliki metode yang diperlukan untuk menghadapinya. DAPATKAN, POST, PUT, DELETE dll yang akan mengembalikan, menyertakan, memperbarui, menghapus sumber daya hubungan.

Sumber TeamsPlayersdaya mengimplementasikan beberapa pertanyaan, seperti GET /TeamsPlayers?player={playerId}mengembalikan semua TeamsPlayershubungan yang diidentifikasi oleh pemain {playerId}. Mengikuti ide yang sama, gunakan GET /TeamsPlayers?team={teamId}untuk mengembalikan semua TeamsPlayersyang telah dimainkan di {teamId}tim. Dalam salah satu GETpanggilan, sumber daya TeamsPlayersdikembalikan. Semua data yang terkait dengan hubungan dikembalikan.

Saat memanggil GET /Players/{playerId}/Teams(atau GET /Teams/{teamId}/Players), sumber daya Players(atau Teams) menelepon TeamsPlayersuntuk mengembalikan tim (atau pemain) terkait menggunakan filter kueri.

GET /Players/{playerId}/Teams bekerja seperti ini:

  1. Temukan semua TeamsPlayers yang pemainnya memiliki id = playerId . ( GET /TeamsPlayers?player={playerId})
  2. Ulangi TeamsPlayers yang dikembalikan
  3. Menggunakan teamId yang diperoleh dari TeamsPlayers , panggil GET /Teams/{teamId}dan simpan data yang dikembalikan
  4. Setelah loop selesai. Kembalikan semua tim yang ada di loop.

Anda dapat menggunakan algoritma yang sama untuk mendapatkan semua pemain dari tim, saat memanggil GET /Teams/{teamId}/Players, tetapi bertukar tim dan pemain.

Sumber daya saya akan terlihat seperti ini:

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

Solusi ini hanya mengandalkan sumber daya REST. Meskipun beberapa panggilan tambahan mungkin diperlukan untuk mendapatkan data dari pemain, tim, atau hubungan mereka, semua metode HTTP mudah diterapkan. POST, PUT, DELETE sederhana dan mudah.

Setiap kali hubungan dibuat, diperbarui atau dihapus, keduanya Playersdan Teamssumber daya diperbarui secara otomatis.

Haroldo Macedo
sumber
benar-benar masuk akal untuk memperkenalkan sumber daya TeamsPlayers. Seksual
vijay
Penjelasan terbaik
Diana
1

Saya tahu bahwa ada jawaban yang ditandai sebagai diterima untuk pertanyaan ini, namun, inilah cara kami dapat menyelesaikan masalah yang diajukan sebelumnya:

Katakanlah untuk PUT

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

Sebagai contoh, semua tindak lanjut akan menghasilkan efek yang sama tanpa perlu disinkronkan karena dilakukan pada satu sumber daya:

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

sekarang jika kita ingin memperbarui banyak keanggotaan untuk satu tim, kita dapat melakukan hal berikut (dengan validasi yang tepat):

PUT    /membership/teams/team1/

{
    membership: [
        {
            teamId: "team1"
            playerId: "player1"
        },
        {
            teamId: "team1"
            playerId: "player2"
        },
        ...
    ]
}
Heidar Pirzadeh
sumber
-3
  1. / pemain (adalah sumber utama)
  2. / tim / {id} / pemain (adalah sumber daya hubungan, sehingga bereaksi berbeda 1)
  3. / keanggotaan (adalah hubungan tetapi rumit secara semantik)
  4. / players / keanggotaan (adalah hubungan tetapi rumit secara semantik)

Saya lebih suka 2

MoaLaiSkirulais
sumber
2
Mungkin saya hanya tidak mengerti jawabannya, tetapi posting ini sepertinya tidak menjawab pertanyaan.
BradleyDotNET
Ini tidak memberikan jawaban untuk pertanyaan itu. Untuk mengkritik atau meminta klarifikasi dari seorang penulis, tinggalkan komentar di bawah posting mereka - Anda selalu dapat mengomentari posting Anda sendiri, dan begitu Anda memiliki reputasi yang cukup, Anda akan dapat mengomentari setiap posting .
Argumen Ilegal
4
@IllegalArgument Ini adalah jawaban dan tidak masuk akal sebagai komentar. Namun, itu bukan jawaban terbaik.
Qix - MONICA DISEBUTKAN
1
Jawaban ini sulit diikuti dan tidak memberikan alasan.
Venkat D.
2
Ini tidak menjelaskan atau menjawab pertanyaan yang diajukan sama sekali.
Manjit Kumar