RESTful API'sında çoktan çoğa ilişkiler nasıl ele alınır?


288

Oyuncuların birden fazla takımda olabileceği Oyuncu ve Takım olmak üzere 2 kurumunuz olduğunu düşünün . Veri modelimde, her varlık için bir tablo ve ilişkileri korumak için bir birleştirme tablosu var. Hazırda bekletme durumu bu iş için uygundur, ancak bu ilişkiyi RESTful API'de nasıl gösterebilirim?

Birkaç yol düşünebilirim. İlk olarak, her bir varlığın diğerinin listesini içermesini sağlayabilirim, bu nedenle bir Player nesnesinin ait olduğu Takımların bir listesi olur ve her Team nesnesinin kendisine ait olan Oyuncuların bir listesi olur. Ekibe bir Oyuncu eklemek için, oyuncunun temsilini, bir isteğin yükü olarak uygun nesne ile POST /playerveya POST gibi bir bitiş noktasına POST edersiniz /team. Bu benim için en "RESTful" gibi görünüyor ama biraz garip geliyor.

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

Bunu yapmanın başka bir yolu da ilişkiyi kendi başına bir kaynak olarak ortaya koymak olacaktır. Dolayısıyla, belirli bir takımdaki tüm oyuncuların bir listesini görmek için bir GET /playerteam/team/{id}veya benzeri bir şey yapabilir ve PlayerTeam varlıklarının bir listesini geri alabilirsiniz. Bir takıma oyuncu eklemek için /playerteam, yük olarak uygun şekilde oluşturulmuş bir PlayerTeam varlığına sahip 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'        
]

Bunun için en iyi uygulama nedir?

Yanıtlar:


129

RESTful arabiriminde, bu ilişkileri bağlantılar olarak kodlayarak kaynaklar arasındaki ilişkileri açıklayan belgeleri döndürebilirsiniz. Böylece, bir takımın, takımdaki /team/{id}/playersoyunculara ( /player/{id}) bağlantıların bir listesi olan bir doküman kaynağına ( ) sahip olduğu ve bir oyuncunun bir doküman kaynağına (/player/{id}/teams) bu, oyuncunun üyesi olduğu takımların bağlantılarının bir listesidir. Güzel ve simetrik. Bir şeyleri kolaylaştırırsa, bu listede yer alan harita işlemlerini yeterince kolayca yapabilirsiniz, hatta bir ilişkiye kendi kimlikleri bile verebilirsiniz (muhtemelen önce takım mı yoksa ilk önce oyuncu mı ilişkisini düşünüp düşünmediğinize bağlı olarak iki kimliği olacaktır). . Tek zor bit, bir ucundan silerseniz, ancak altta yatan bir veri modeli kullanarak ve daha sonra REST arabiriminin bir görünümü olmasını sağlayarak, ilişkiyi diğer uçtan silmeyi hatırlamanız gerektiğidir. bu model bunu kolaylaştıracak.

İlişki kimlikleri, takımlar ve oyuncular için kullandığınız her tür kimliğe bakılmaksızın UUID'lere veya eşit derecede uzun ve rastgele bir şeye dayanmalıdır. Yani (küçük tamsayılar do sen çarpışmaları konusunda endişelenmeden ilişkinin her bir ucu için kimlik bileşeni olarak aynı UUID kullanmak sağlayacak değil o avantajı var). Bu üyelik ilişkilerinin, bir oyuncuyu ve bir takımı çift yönlü bir şekilde ilişkilendirmelerinin dışında herhangi bir özelliğe sahip olması durumunda, hem oyunculardan hem de takımlardan bağımsız olan kendi kimliğine sahip olmalıdırlar; oynatıcıdaki bir GET »takım görünümü ( /player/{playerID}/teams/{teamID}) daha sonra çift yönlü görünüme ( /memberships/{uuid}) HTTP yönlendirmesi yapabilir .

XLink xlink:href özniteliklerini kullanarak döndürdüğünüz (elbette XML üretiyorsanız) XML belgelerine bağlantılar yazmanızı öneririm .


265

Ayrı bir /memberships/kaynak seti yapın .

  1. REST, başka bir şey yoksa evrilebilir sistemler yapmakla ilgilidir. Bu anda, sadece belirli bir oyuncu belirli bir takımda olması umurumda olabilir, ancak gelecekte bir noktada, sen olacaktır daha verilerle bu ilişkiyi açıklamak istiyorum: onlar bu takımda ne kadar zamandır onları sevk o takıma koçu kim / kim o takımdayken, vb.
  2. REST, önbellek atomisitesi ve geçersiz kılma için biraz dikkate alınması gereken verimlilik için önbelleğe alınmasına bağlıdır. Bu /teams/3/players/listeye yeni bir varlık GÖNDERSİNİZ geçersiz kılınır, ancak alternatif URL'nin /players/5/teams/önbelleğe alınmasını istemezsiniz . Evet, farklı önbelleklerin her bir listenin farklı yaşlardaki kopyaları olacaktır ve bununla ilgili yapabileceğimiz çok şey yoktur, ancak en azından geçersiz kılmamız gereken varlık sayısını sınırlayarak güncellemeyi POST'layan kullanıcı için karışıklığı en aza indirebiliriz. onların müşterinin yerel önbellekte bir ve sadece bir tanesi de /memberships/98745(içinde "alternatif endeks" nin HELLAND açıklamalarına bakınız Dağıtılmış İşlemler ötesinde Hayat daha detaylı tartışma için).
  3. Yukarıdaki 2 noktayı yalnızca /players/5/teamsveya öğesini seçerek uygulayabilirsiniz /teams/3/players(ancak her ikisini birden değil). İlkini varsayalım. Bununla birlikte, bir noktada, mevcut üyeliklerin /players/5/teams/listesini ayırmak ve yine de bir yerlerde geçmiş üyeliklere başvurabilirsiniz . Kaynaklara köprülerin bir listesini yapın ve daha sonra istediğiniz zaman, bireysel üyelik kaynakları için herkesin yer işaretlerini kırmak zorunda kalmadan ekleyebilirsiniz . Bu genel bir kavramdır; Özel durumunuz için daha geçerli olan benzer gelecekleri hayal edebileceğinizden eminim./players/5/memberships//memberships/{id}//players/5/past_memberships/

11
Nokta 1 ve 2 mükemmel bir şekilde açıklanmıştır, teşekkürler, eğer kimse gerçek yaşam deneyiminde 3. nokta için daha fazla ete sahipse, bu bana yardımcı olacaktır.
Alain

2
En iyi ve en basit cevap IMO teşekkürler! İki uç noktaya sahip olmak ve bunları senkronize tutmak bir takım komplikasyonlara sahiptir.
Venkat D.

7
merhaba fumanchu. Sorular: Geri kalan bitiş noktasında / üyeliklerde / 98745 URL'nin sonundaki bu sayı neyi temsil ediyor? Üyelik için benzersiz bir kimlik mi? Üyelik uç noktasıyla nasıl etkileşim kurulur? Bir oyuncu eklemek için {team: 3, player: 6} ile bir yük içeren bir POST gönderilecek ve böylece ikisi arasında bağlantı oluşturuluyor mu? GET ne olacak? sonuç almak için / memberships? player = ve / membersihps? team = 'e bir GET gönderir misiniz? Fikir bu mu? Bir şey eksik mi? (Dinlendirici son noktaları öğrenmeye çalışıyorum) Bu durumda, üyeliklerde 98745/98745 kimliği gerçekten yararlı mı?
aruuuuu

@aruuuuu vekil bir PK ile dernek için ayrı bir uç nokta sağlanmalıdır. Genel olarak hayatı da çok daha kolay hale getirir: / Memberships / {memberId}. Anahtar (playerId, teamId) benzersiz kalır ve bu nedenle bu ilişkiye sahip olan kaynaklarda kullanılabilir: / teams / {teamId} / players ve / players / {playerId} / teams. Ancak bu ilişkiler her iki tarafta da her zaman sürdürülmez. Örneğin, Tarifler ve Malzemeler: / maddeler / {componententId} / yemek tarifleri / kullanmanız neredeyse hiç gerekmeyecek.
Alexander Palamarchuk

65

Ben alt kaynaklar ile böyle bir ilişki harita olurdu, genel tasarım / çaprazlama o zaman şöyle olurdu:

# team resource
/teams/{teamId}

# players resource
/players/{playerId}

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

Restful açısından SQL ve birleştirmeleri düşünmemek, daha çok koleksiyonlara, alt koleksiyonlara ve geçişlere çok yardımcı olur.

Bazı örnekler:

# 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

Gördüğünüz gibi POST'u oyuncuları takımlara yerleştirmek için değil, n: n oyuncu ve takım ilişkinizi daha iyi ele alan PUT'u kullanıyorum.


20
Team_player durum vb. Gibi ek bilgilere sahipse ne olur? onu modelinizde nerede temsil ediyoruz? bir kaynağa tanıtabilir ve oyun /, oyuncu gibi URL'leri sağlayabilir miyiz?
Narendra Kamma

Hey, hızlı bir soru sadece bu doğru anladığımdan emin olmak için: GET / takımlar / 1 / oyuncular / 3 boş bir yanıt gövdesi döndürür. Bundan tek anlamlı cevap 200'e karşılık 404'tür. Oyuncu varlığının bilgileri (isim, yaş, vb.) GET / takımlar / 1 / oyuncular / 3 tarafından döndürülmez. Müşteri oyuncu hakkında ek bilgi almak istiyorsa, GET / player / 3'ü alması gerekir. Bunların hepsi doğru mu?
Verdagon

2
Haritalandırmanıza katılıyorum, ancak bir sorum var. Kişisel görüş meselesi, ama POST / takımlar / 1 / oyuncular hakkında ne düşünüyorsunuz ve neden kullanmıyorsunuz? Bu yaklaşımda herhangi bir dezavantaj / yanıltıcı görüyor musunuz?
JakubKnejzlik

2
POST idempotent değildir, yani POST / takımlar / 1 / oyuncuları n kez yaparsanız, n-kez / takımlar / 1'i değiştirirsiniz. ancak bir oyuncuyu / takımlara / 1 kez n taşımak, takımın durumunu değiştirmez, bu nedenle PUT kullanımı daha açıktır.
manuel aldana

1
@NarendraKamma statusPUT isteği sadece param olarak göndermek varsayalım ? Bu yaklaşımın bir dezavantajı var mı?
Traxo

22

Mevcut cevaplar tutarlılık ve dikkatsizlik rollerini açıklamamaktadır - bu UUIDskimlikleri kimlikler için / PUTyerine rastgele sayılar motive eder POST.

"" Gibi basit bir senaryomuzun olduğu durumu ele alırsak, Bir ekibe yeni bir oyuncu ekle tutarlılık sorunları ile karşılaşırız.

Oyuncu mevcut olmadığından, şunları yapmamız gerekir:

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

Ancak, sonra istemci işlemi başarısız gerektiğini POSTüzere /players, bir ekip ait olmayan bir oyuncu oluşturduk:

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

Şimdi artık yetim bir çift oyuncumuz var /players/5 .

Bunu düzeltmek için bazı doğal anahtarlarla eşleşen yetim oyuncuları kontrol eden özel kurtarma kodu yazabiliriz (ör. Name ). Bu test edilmesi gereken özel kod, daha fazla para ve zaman vb.

Özel kurtarma koduna ihtiyaç duymamak için, PUT bunun yerinePOST .

Gönderen RFC :

amacı PUT kasıtlı

Bir işlemin idempotent olması için, sunucu tarafından oluşturulan kimlik dizileri gibi harici verileri hariç tutması gerekir. Bu insanlar hem tavsiye neden olduğu PUTve UUIDiçin s Idbirlikte s.

Bu bize hem yeniden sağlar /players PUTve /memberships PUTsonuçları olmadan:

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

Her şey yolunda ve kısmi arızalar için yeniden denemekten başka bir şey yapmamız gerekmiyordu.

Bu, mevcut cevaplara bir eklentidir, ancak umarım onları ne kadar esnek ve güvenilir ReST olabileceğinin daha büyük resmi bağlamında ortaya koyar.


Bu varsayımsal uç noktada, nereden aldın 23lkrjrqwlej?
cbcoutinho

1
klavyede yüz rulo - 23lkr hakkında özel bir şey yok ... gobbledegook sıralı veya anlamlı değil dışında
Seth

9

Benim tercih solüsyon üç kaynak oluşturmaktır: Players, Teamsve TeamsPlayers.

Yani, bir takımın tüm oyuncularını almak için, sadece Teamskaynağa gidin ve arayarak tüm oyuncularını alın GET /Teams/{teamId}/Players.

Öte yandan, bir oyuncunun oynadığı tüm takımları almak için Teamskaynağı içine alın Players. Arayın GET /Players/{playerId}/Teams.

Ve, çoktan çoğa ilişki çağrısını almak için GET /Players/{playerId}/TeamsPlayersveya GET /Teams/{teamId}/TeamsPlayers.

Bu çözümde, aradığınızda GET /Players/{playerId}/Teams, aradığınızda aldığınız Teamskaynakla aynı olan bir dizi kaynak elde edeceğinizi unutmayın GET /Teams/{teamId}. Tersi aynı prensibi takip eder, Playersçağrı yaparken bir dizi kaynak alırsınızGET /Teams/{teamId}/Players .

Her iki çağrıda da ilişki hakkında hiçbir bilgi döndürülmez. Örneğin, hayırcontractStartDate döndürülür, çünkü döndürülen kaynağın ilişki hakkında yalnızca kendi kaynağı hakkında bilgi yoktur.

Nn ilişkisi ile başa çıkmak için, ya GET /Players/{playerId}/TeamsPlayersda 'i arayın GET /Teams/{teamId}/TeamsPlayers. Bu çağrılar tam olarak kaynağı döndürür,TeamsPlayers .

Bu TeamsPlayerskaynak vardır id, playerId,teamId nitelikleri, yanı sıra bazı diğerleri ilişkiyi açıklamak için. Ayrıca, onlarla başa çıkmak için gerekli yöntemlere sahiptir. GET, POST, PUT, DELETE vb döndürecek, dahil edecek, güncelleyecek, ilişki kaynağını kaldıracaktır.

TeamsPlayersKaynak uygular bazı sorgular, ister GET /TeamsPlayers?player={playerId}tüm dönmek TeamsPlayerstarafından tespit oyuncu ilişkileri {playerId}vardır. Aynı fikri takiben, takımda oynanan GET /TeamsPlayers?team={teamId}her şeyi geri döndürmek için kullanın . Her iki çağrıda da kaynak döndürülür. İlişki ile ilgili tüm veriler iade edilir.TeamsPlayers{teamId}GETTeamsPlayers

Arama yaparken GET /Players/{playerId}/Teams(veya GET /Teams/{teamId}/Players), kaynak Players(veya Teams) TeamsPlayersbir sorgu filtresi kullanarak ilgili takımları (veya oyuncuları) geri döndürmeye çağırır .

GET /Players/{playerId}/Teams şu şekilde çalışır:

  1. Bütün bul TeamsPlayers o oyuncu vardır id = playerid . ( GET /TeamsPlayers?player={playerId})
  2. Döndürülen Takımları Oynatın
  3. TeamsPlayers'dan elde edilen teamId'i kullanarak, iade edilen GET /Teams/{teamId}verileri arayın ve saklayın
  4. Döngü bittikten sonra. Döngüdeki tüm takımları iade et.

Aynı algoritmayı, bir takımdan tüm oyuncuları çağırırken GET /Teams/{teamId}/Players, ancak takım ve oyuncu alışverişinde bulunmak için kullanabilirsiniz.

Kaynaklarım şöyle görünecektir:

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

Bu çözüm yalnızca REST kaynaklarına dayanmaktadır. Oyunculardan, takımlardan veya ilişkilerinden veri almak için bazı ekstra çağrılar gerekli olsa da, tüm HTTP yöntemleri kolayca uygulanır. POST, PUT, DELETE basit ve anlaşılır.

Bir ilişki oluşturulduğunda zaman, güncellenmiş veya silinmiş, hem Playersve Teamskaynaklar otomatik olarak güncellenir.


TeamsPlayers kaynağını tanıtmak gerçekten mantıklı. Korkunç
vijay

En iyi açıklama
Diana

1

Bu soru için kabul edilmiş olarak işaretlenmiş bir yanıt olduğunu biliyorum, ancak daha önce ortaya çıkan sorunları nasıl çözebileceğimiz aşağıda açıklanmıştır:

Diyelim ki PUT için

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

Örnek olarak, aşağıdakilerin hepsi senkronizasyona gerek kalmadan aynı etkiye neden olacaktır, çünkü bunlar tek bir kaynak üzerinde yapılır:

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

şimdi bir ekip için birden fazla üyeliği güncellemek istiyorsak aşağıdaki işlemleri yapabiliriz (doğru doğrulamalarla):

PUT    /membership/teams/team1/

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

-3
  1. / players (ana kaynaktır)
  2. / teams / {id} / players (bir ilişki kaynağıdır, dolayısıyla 1'den farklı tepki gösterir)
  3. / Memberships (bir ilişkidir ancak anlamsal olarak karmaşıktır)
  4. / oyuncular / üyelikler (bir ilişkidir ancak anlamsal olarak karmaşıktır)

2'yi tercih ederim


2
Belki de cevabı anlamıyorum, ancak bu yazı soruyu cevaplamıyor gibi görünüyor.
BradleyDotNET

Bu soruya bir cevap sağlamaz. Bir yazardan eleştiri veya açıklama istemek için gönderilerinin altına bir yorum bırakın - her zaman kendi yayınlarınıza yorum yapabilirsiniz ve yeterli bir üne sahip olduğunuzda herhangi bir yazıya yorum yapabilirsiniz .
Yasadışı Argüman

4
@IllegalArgument O olduğu bir cevap ve bir yorum olarak anlamlı olmaz. Ancak, en büyük cevap bu değildir.
Qix - MONICA

1
Bu cevabı takip etmek zordur ve sebep göstermez.
Venkat D.

2
Bu, sorulan soruyu hiç açıklamaz veya cevaplamaz.
Manjit Kumar
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.