2012-11-05 4 views
19

Pracuję nad REST API i staram się zrozumieć, jak radzić sobie z zasobami hierarchicznymi.Uaktualnianie/tworzenie zasobów hierarchicznych REST

Tło

Zacznijmy od prostego przykładu. W moim API mam użytkowników, Profile użytkowników i Recenzje.

  • Użytkownicy musi mieć profil użytkownika związany (a profil użytkownika odpowiada tylko jeden użytkownik)
  • Użytkownicy może mieć Przegląd związany (a weryfikacja odpowiada tylko jeden użytkownik)
  • reprezentacja

użytkownika zasób powinien być:

User: { 
    "u1": "u1value", // User's attributes 
    "u2": "u2value", 
    ... 
    "links": [{ 
     "rel": "profile", 
     "href": "http://..." // URI of the profile resource 
    }, { 
     "rel": "review", 
     "href": "http://..." // URI of the review resource 
    }] 
} 

Profil użytkownika reprezentacja zasobów powinno być: reprezentacja

UserProfile: { 
    "p1": "p1value", // Profile attributes 
    "p2": "p2value", 
    ... 
    "links": [{ 
     "rel": "owner", 
     "href": "http://..." // URI of the user resource 
    }] 
} 

weryfikacja zasobów powinno być:

Review: { 
    "r1": "r1value", // Review attributes 
    "r2": "r2value", 
    ... 
    "links": [{ 
     "rel": "owner", 
     "href": "http://..." // URI of the user resource 
    }] 
} 

Resources URI mogą być:

  • http://api.example.com/users/{userid}: dostęp do zasobu użytkownika
  • http://api.example.com/users/{userid}/profile: dostęp do zasobu profil użytkownika
  • http://api.example.com/users/{userid}/review: dostęp do przeglądu zasobu użytkownika

tworzenie zasobu: jaki jest właściwy sposób utworzyć użytkownika?

Teraz chcę utworzyć nowego użytkownika:

  1. POST http://api.example.com/users {"u1": "bar", "u2": "foo"} i wrócę nowy userid = 42
  2. POST http://api.example.com/users/42/profile {"p1": "baz", "p2": "asd"}
  3. PUT http://api.example.com/users {"u1": "bar", "u2": "foo", links: [{"rel": "profile", "href": "http://api.example.com/users/42/profile"]}

Moje obawy:

  • Co jeśli coś dzieli się między 1 a 2 lub 2 i 3?
  • W 3), czy serwer powinien zaktualizować automatycznie linki w http://api.example.com/users/42/profile, aby wskazać właściwego właściciela?
  • Aktualizowanie pól linków jest właściwym sposobem tworzenia relacji? Czy powinienem pominąć krok 3) i pozwolić systemowi odgadnąć relacje zgodnie z konwencjami URI? (Czytałem w kilku książkach, że URI należy uznać za nieprzejrzysty.)
+0

jaki jest powód oddzielania obiektów dla użytkownika i profilu, ponieważ wydają się mieć obowiązkową relację 1 do 1? – njzk2

Odpowiedz

15

Twoje wątpliwości są dobrze uporządkowane, a lista problemów jest poprawna. Jeśli mogę zasugerować, twoje podejście wygląda bardzo podobnie do używania relacyjnego podejścia DB i robisz INSERT, pobierając PK z sekwencji, której używasz do następnego INSERT, i tak dalej.

Niech serwer utrzymania więzów integralności

Jako obserwacji, nawet jeśli po oryginalny system, opuścić krok 3 całkowicie. Identyfikator URI w polu links, który jest widoczny podczas pobierania dokumentu użytkownika, powinien zostać wygenerowany przez serwer na podstawie istnienia rekordu profilu.

Na przykład, jeśli korzystasz z relacyjnego zaplecza, wybieramy WYBIERZ z UŻYTKOWNIKÓW, aby uzyskać rekord użytkownika. Następnie WYBIERZ Z PROFILI. Jeśli istnieje rekord, modyfikujesz strukturę danych powrotu, aby uwzględnić odwołanie.

POST całe dokumenty

powszechnym sposobem rozwiązać inne problemy, które po otwarciu jest umożliwienie delegowania na całego dokumentu do adresu URL użytkownika (takich jak bazy danych NoSQL takich jak MongoDB). Oto dokument jest użytkownik i profil:

{ 
    "u1": "bar", 
    "u2": "foo", 
    "profile": { 
       "p1": "baz", 
       "p2": "asd" 
       } 
} 

W tym podejściu, twój punkt końcowy na serwerze otrzymuje strukturę zagnieżdżonych (document) i wykonuje wstawić do użytkowników, pobiera PK, a następnie wykonuje INSERT do PROFILI używając tego PK. Robi to po stronie serwera rozwiązuje kilka problemów:

  1. transakcji mogą być atomowy
  2. Istnieje tylko jedna wymiana sieci między klientem a serwerem
  3. Serwer robi dźwiganie ciężarów i może zatwierdzić całą transakcję (na przykład użytkownik nie jest tworzony, jeśli profil nie jest ważna)
  4. nie potrzeba kroku 3.

pamiętać, że takie podejście jest oprócz API, które opisałeś powyżej - nadal chcesz mieć możliwość bezpośredniego dostępu do profilu użytkownika.

GET - klient może określić pola

Interesujące jest porównanie z API z dobrze znanymi firmami. Weźmy na przykład LinkedIn. W ich developer API domyślny GET dla użytkownika zwraca po prostu nazwę użytkownika, nagłówek i URI.

Jednakże, jeśli żądanie określa dodatkowe pola, można uzyskać zagnieżdżone dane, np. Drugi przykład w http://developer.linkedin.com/documents/understanding-field-selectors zwraca nazwę użytkownika i listę firmowych nazw dla posiadanych przez siebie pozycji. Można wdrożyć podobny schemat dla profili i recenzji.

łata za aktualizowanie właściwości dokumentu

Przy wkładaniu i zapytań z drogi, to może być warte rozważenia sposobu aktualizacji danych (Patch). Nadpisywania pole jest oczywiste, więc można, na przykład łatkę http://api.example.com/users/42 następujące:

{ 
    "u1": null, 
    "u2": "new-foo", 
    "profile": { "p1": "new-baz"} 
} 

Które rozbroić u1 ustaw u2 do new-foo i zaktualizować profil użytkownika p1 do new-baz. Zauważ, że jeśli brakuje pola (p2), pole nie zostanie zmienione. PATCH jest lepszy od starszego PUT, jak wyjaśniono w this answer.

Jeśli trzeba tylko zaktualizować profil, patch nowy rekord profilu bezpośrednio do http://api.example.com/users/42/profile

DELETE powinna kaskadowo

Wreszcie, usuwanie można zrobić z metody DELETE wskazując na zasób, który chcesz usunąć - czy to Użytkownik, Profil czy Recenzja. Zaimplementuj usuwanie kaskadowe, aby usunięcie użytkownika spowodowało usunięcie jego profilu i recenzji.

+1

Proponuję użyć metody 'PATCH' dla operacji, które częściowo lub przyrostowo modyfikują zasób. 'PUT' sugeruje całkowitą wymianę. –

+0

@TimothyShields: Dzięki - zupełnie to przegapiłem. Aktualizuję teraz odpowiedź. –

+0

Dobra odpowiedź. IMO brakuje tylko struktury podobnej do formy (podobnej do "linków" w przykładach pytań), która mówi klientowi, jak i co POST stworzyć użytkownikowi. Byłoby sensownie uwzględnić to w odpowiedzi na GET na 'http: // api.example.com/users /' –

0

Właściwie od pomyślnego kroku (1) powinieneś otrzymać kod HTTP 201 Stworzony i adres (URL) do nowo utworzonego zasobu, a nie tylko numer ID. Jeśli krok (2) nie powiedzie się, interfejs REST API powinien wskazać, czy problem dotyczy klienta, na przykład źle sformułowany dokument (kod problemu 4xx) lub serwer (5xx). Na przykład, jeśli w międzyczasie usunięto zasób 42, należy zwrócić kod 404 Not Found.

Na tym polega problem z bezpaństwowymi interfejsami REST API - nie mogą obsługiwać transakcji złożonych z więcej niż jednego żądania. Aby było to możliwe, musisz utrzymywać sesję (stan) na serwerze.

Nawiasem mówiąc, adres URL w kroku (3) w twoim przykładzie sugeruje, że zastępujesz wszystkich użytkowników i prawdopodobnie powinieneś przeczytać http://api.example.com/users/42.

Masz wybór między przesłaniem kompletnego użytkownika + dokumentu profilu, który zostanie podzielony na dwa rekordy bazy danych w jednej transakcji atomowej, lub aby umożliwić zachowanie częściowych danych użytkownika, tj. Użytkownika bez profilu.

Wybór zależy od kontekstu. Na przykład może być zupełnie dobrze, że użytkownik nie ma profilu (więc może być dostarczony przez użytkownika). I odwrotnie, posiadanie rekordu profilu, który nie należy do żadnego użytkownika, jest prawdopodobnie nie do zaakceptowania. Dyskusja na temat egzekwowania tej logiki wykracza poza zakres twojego pytania i będzie się różnić w zależności od wybranego typu magazynu trwałego (bazy danych). Relacyjne bazy danych wymuszają to za pomocą kluczy obcych.

0

wierzę wasze rozmowy powinny być jak ten

1) utworzyć użytkownika

POST http://api.example.com/users + params in payload 

Jeśli zwróci HTTP id 201 + użytkownika, a następnie można przejść dalej i utworzyć profil. Inaczej traktujesz wyjątek tak, jak chcesz. Powinieneś poczekać na pierwsze połączenie, aby wrócić przed rozpoczęciem tworzenia profilu.

2) Utwórz profil skojarzony z użytkownikiem 42 (jeśli powstanie użytkownik było ok)

POST http://api.example.com/users/42/profile + params in payload 

powraca HTTP 201 + profil id

Twój backend będzie odpowiedzialny za aktualizowanie obiekt użytkownika i obiekt profilu (i twoja baza danych), aby użytkownicy 42 byli połączeni z nowym profilem. Jeśli backend nie może połączyć obiektu, możesz wysłać błąd 500 wyjaśniający, co się stało.

Więc moim zdaniem byłoby pominąć krok 3.

Teraz rozumiem, że chodzi o to o tym, że użytkownik MUST mieć profil

widzę 2 rozwiązania

1) możesz utworzyć pusty skojarzony profil podczas tworzenia swojego użytkownika. Następnie, wysyłając zapytanie do użytkownika, można uzyskać identyfikator profilu i zmodyfikować go za pomocą PUT (naprawdę nie podoba mi się to rozwiązanie, ponieważ gdy pytasz API o utworzenie użytkownika, powinien on jedynie stworzyć użytkownika i nic więcej, ale w rzeczywistości profil jest obowiązkowy, nie jest tak brzydki jak się wydaje).

2) możesz mieć właściwość, w której użytkownik mówi, czy użytkownik jest poprawny, czy nie (co oznacza, że ​​ma powiązany profil). Pod koniec procesu tworzenia, jeśli użytkownik 42 nie jest poprawny, możesz go usunąć lub ponowić utworzenie profilu ... Następnie możesz zapytać tylko poprawnych użytkowników o czymś w rodzaju/users? IsCorrect = true

3) Pozwól klient traktować fakt, że użytkownik w żadnym profilu -> Pokaż popup prosić o stworzeniu profilu ...

Wystarczy popatrzeć na to document REST API dla najlepszych praktyk

Może też mieć spójrz na HAL, który próbuje poradzić sobie z relacją między obiektami.

I na koniec możesz przeczytać api-craft google group, aby znaleźć interesujące tematy związane z Twoim problemem.

2

należy trzymać się HATEOAS i nieprawidłowego URL można uzyskać na swoich odpowiedziach:

Dla ułatwienia dostępu, powiedzmy User.profile zawiera href z link z rel == profile.

utworzyć użytkownika

ze stanowiskiem opisałeś ... ale nie powinna powrócić identyfikator, ale użytkownik, wraz z jego łącza.

User: { 
    "u1": "bar", // User's attributes 
    "u2": "foo", 
    ... 
    "profile": "http://api.example.com/users/42/profile", 
    "links": [{ 
     "rel": "profile", 
     "href": "http://api.example.com/users/42/profile" 
    }, 
    ... 
    ] 
} 

W tym momencie zasób profil na User.profile (może być http://api.example.com/users/42/profile, czy cokolwiek lokalizacja migracji w przyszłości), to być cokolwiek domyślny profil powinien być, na przykład pusty dokument lub z wypełnionym tylko linkiem właściciela.

Aktualizacja profilu

profile = GET User.profile 
profile.p1 = "baz" 
profile.p2 = "asd" 
PUT profile to the same url you just dereferenced 

Przez dereferencing HREF na dokumentach zamiast budowania adresów URL z identyfikatora można uzyskać od odpowiedzi, ty klient nie będzie musiał zmieniać, gdy zmiany API. Podobnie jak w przypadku gdy - profile zostaną przeniesione do http://profiles.cdn.example.com/ - profile uzyskają wartość p3

"Stare" klientów API będzie działało bez konieczności zmiany jakiegokolwiek kodu.