REST - umieścić identyfikatory w treści, czy nie?

101

Powiedzmy, że chcę mieć zasób RESTful dla ludzi, w którym klient może przypisać identyfikator.

Osoba wygląda tak: {"id": <UUID>, "name": "Jimmy"}

W jaki sposób klient powinien go zapisać (lub „PUT”)?

  1. PUT /person/UUID {"id": <UUID>, "name": "Jimmy"} - teraz mamy tę paskudną duplikację, którą musimy cały czas weryfikować: Czy identyfikator w treści jest zgodny z tym na ścieżce?
  2. Asymetryczna reprezentacja:
    • PUT /person/UUID {"name": "Jimmy"}
    • GET /person/UUID zwroty {"id": <UUID>, "name": "Jimmy"}
  3. Brak identyfikatorów w treści - identyfikator tylko w lokalizacji:
    • PUT /person/UUID {"name": "Jimmy"}
    • GET /person/UUID zwroty {"name": "Jimmy"}
  4. Nie POSTwydaje się to dobrym pomysłem, ponieważ identyfikator jest generowany przez klienta.

Jakie są typowe wzorce i sposoby rozwiązania tego problemu? Identyfikatory tylko w lokalizacji wydają się najbardziej dogmatycznie poprawnym sposobem, ale utrudniają też praktyczną implementację.

Konrad Garus
źródło

Odpowiedzi:

65

Nie ma nic złego w posiadaniu różnych modeli odczytu / zapisu: klient może napisać jedną reprezentację zasobu, po której serwer może zwrócić inną reprezentację z dodanymi / obliczonymi elementami (lub nawet zupełnie inną reprezentację - w żadnej specyfikacji nie ma nic przeciwko temu) , jedynym wymaganiem jest, aby PUT utworzył lub zastąpił zasób).

Więc wybrałbym rozwiązanie asymetryczne w (2) i unikałbym "nieprzyjemnego sprawdzania duplikacji" po stronie serwera podczas pisania:

PUT /person/UUID {"name": "Jimmy"}

GET /person/UUID returns {"id": <UUID>, "name": "Jimmy"}
Jørn Wildt
źródło
2
A jeśli zastosujesz typowanie (statyczne lub dynamiczne), nie możesz bezboleśnie mieć modeli bez identyfikatora ... Więc znacznie łatwiej jest usunąć ID z adresu URL dla żądań PUT. Nie będzie to „uspokajające”, ale będzie poprawne.
Ivan Kleshnin
2
Zachowaj dodatkowy termin TO bez idrazem z TO z identyfikatorem i jednostką oraz dodatkowymi konwerterami i zbyt dużym narzutem dla programistów.
Grigorij Kislin
Co jeśli otrzymam identyfikator od BODY np .: PUT / osoba {"id": 1, "name": "Jimmy"}. Czy to byłaby zła praktyka?
Bruno Santos
Umieszczenie identyfikatora w ciele byłoby w porządku. Użyj identyfikatora GUID zamiast liczby całkowitej - w przeciwnym razie istnieje ryzyko zduplikowania identyfikatorów.
Jørn Wildt
To jest źle. Zobacz moją odpowiedź. PUT musi zawierać cały zasób. Użyj PATCH, jeśli chcesz wykluczyć identyfikator i zaktualizować tylko części rekordu.
CompEng88
27

Jeśli jest to publiczny interfejs API, odpowiadaj ostrożnie, ale zgadzaj się swobodnie.

Rozumiem przez to, że powinieneś wspierać zarówno 1, jak i 2. Zgadzam się, że 3 nie ma sensu.

Sposobem na obsługę zarówno 1, jak i 2 jest pobranie identyfikatora z adresu URL, jeśli żaden nie jest podany w treści żądania, a jeśli znajduje się w treści żądania, sprawdź, czy pasuje do identyfikatora w adresie URL. Jeśli te dwa elementy nie są zgodne, zwróć odpowiedź 400 złych żądań.

Zwracając zasób osoby, zachowaj ostrożność i zawsze dołączaj identyfikator w pliku json, nawet jeśli jest on opcjonalny.

Jay Pete
źródło
3
To powinno być przyjęte rozwiązanie. Interfejs API powinien zawsze być przyjazny dla użytkownika. Powinien być opcjonalny w treści. Nie powinienem otrzymywać identyfikatora z POST, a następnie musieć uczynić go niezdefiniowanym w PUT. Ponadto wprowadzony punkt odpowiedzi 400 jest włączony.
Michael
Około 400 kodu patrz także dyskusja softwareengineering.stackexchange.com/questions/329229/ ... Krótko mówiąc, kod 400 nie jest nieodpowiedni, tylko mniej konkretny, w porównaniu do 422.
Grigorij Kislin
8

Jednym z rozwiązań tego problemu jest nieco myląca koncepcja „hipertekstu jako silnika stanu aplikacji” lub „HATEOAS”. Oznacza to, że odpowiedź REST zawiera dostępne zasoby lub akcje do wykonania jako hiperłącza. Korzystając z tej metody, która była częścią pierwotnej koncepcji REST, unikalne identyfikatory / identyfikatory zasobów same są hiperłączami. Na przykład możesz mieć coś takiego:

GET /person/<UUID> {"person": {"location": "/person/<UUID>", "data": { "name": "Jimmy"}}}

Następnie, jeśli chcesz zaktualizować ten zasób, możesz zrobić (pseudokod):

updatedPerson = person.data
updatedPerson.name = "Timmy"
PUT(URI: response.resource, data: updatedPerson)

Zaletą tego jest to, że klient nie musi mieć pojęcia o wewnętrznej reprezentacji identyfikatorów użytkowników na serwerze. Identyfikatory mogą się zmieniać, a nawet same adresy URL mogą się zmieniać, o ile klient ma sposób je odkryć. Na przykład podczas pobierania kolekcji osób możesz zwrócić odpowiedź w następujący sposób:

GET /people
{ "people": [
    "/person/1",
    "/person/2"
  ]
}

(Oczywiście możesz również zwrócić pełny przedmiot osobisty dla każdej osoby, w zależności od potrzeb aplikacji).

Dzięki tej metodzie myślisz o swoich obiektach bardziej w kategoriach zasobów i lokalizacji, a mniej w kategoriach ID. W ten sposób wewnętrzna reprezentacja unikalnego identyfikatora jest oddzielona od logiki klienta. To był pierwotny bodziec dla REST: stworzenie architektur klient-serwer, które są luźniej powiązane niż systemy RPC, które istniały wcześniej, przy użyciu funkcji HTTP. Więcej informacji na temat HATEOAS można znaleźć w artykule w Wikipedii oraz w tym krótkim artykule .

bthecohen
źródło
4

We wstawce nie musisz dodawać identyfikatora w adresie URL. W ten sposób, jeśli wyślesz ID w PUT, możesz zinterpretować jako UPDATE do zmiany klucza podstawowego.

  1. WSTAWIĆ:

    PUT /persons/ 
      {"id": 1, "name": "Jimmy"}
    HTTP/1.1 201 Created     
      {"id": 1, "name": "Jimmy", "other_field"="filled_by_server"}
    
    GET /persons/1
    
    HTTP/1.1 200 OK
      {"id": 1, "name": "Jimmy", "other_field"="filled_by_server"}  
    
  2. AKTUALIZACJA

    PUT /persons/1 
         {"id": "2", "name": "Jimmy Jr"} - 
    HTTP/1.1 200 OK
         {"id": "2", "name": "Jimmy Jr", "other_field"="filled_by_server"}
    
    GET /persons/2 
    
    HTTP/1.1 200 OK
         {"id": "2", "name": "Jimmy Jr", "other_field"="updated_by_server"}
    

Interfejs JSON API korzysta z tego standardu i rozwiązuje niektóre problemy ze zwracaniem wstawionego lub zaktualizowanego obiektu z łączem do nowego obiektu. Niektóre aktualizacje lub wstawki mogą zawierać logikę biznesową, która zmieni dodatkowe pola

Zobaczysz również, że możesz uniknąć pobierania po wstawce i aktualizacji.

borjab
źródło
2

Chociaż dobrze jest mieć różne reprezentacje dla różnych operacji, ogólną rekomendacją dla PUT jest przechowywanie CAŁEGO ładunku . To znaczy, że idpowinno tam być. W przeciwnym razie powinieneś użyć PATCH.

Powiedziawszy to, myślę, że PUT powinien być używany głównie do aktualizacji i idzawsze powinien być również przekazywany w adresie URL. W rezultacie używanie PUT do aktualizacji identyfikatora zasobu jest złym pomysłem. Pozostawia nas w niepożądanej sytuacji, gdy idadres URL może różnić się odid w treści.

Jak więc rozwiązać taki konflikt? Zasadniczo mamy 2 opcje:

  • wyrzuć wyjątek 4XX
  • dodaj nagłówek Warning( X-API-Warnetc).

To tak blisko, jak tylko mogę odpowiedzieć na to pytanie, ponieważ ogólnie temat jest kwestią opinii.

yuranos
źródło
2

FYI, odpowiedzi tutaj są błędne.

Widzieć:

https://restfulapi.net/rest-api-design-tutorial-with-example/

https://restfulapi.net/rest-put-vs-post/

https://restfulapi.net/http-methods/#patch

POŁOŻYĆ

Używaj interfejsów API PUT głównie do aktualizowania istniejącego zasobu (jeśli zasób nie istnieje, API może zdecydować o utworzeniu nowego zasobu lub nie). Jeśli nowy zasób został utworzony przez PUT API, serwer pochodzenia MUSI poinformować agenta użytkownika za pośrednictwem odpowiedzi HTTP 201 (Utworzono), a jeśli istniejący zasób zostanie zmodyfikowany, 200 (OK) lub 204 (Brak treści) NALEŻY wysłać kody odpowiedzi, aby wskazać pomyślne zakończenie żądania.

Jeśli żądanie przechodzi przez pamięć podręczną, a identyfikator URI żądania identyfikuje co najmniej jedną jednostkę aktualnie buforowaną, te wpisy POWINNY być traktowane jako nieaktualne. Odpowiedzi na tę metodę nie podlegają buforowaniu.

Użyj PUT, jeśli chcesz zmodyfikować pojedynczy zasób, który jest już częścią kolekcji zasobów. PUT zastępuje zasób w całości. Użyj PATCH, jeśli żądanie aktualizuje część zasobu.

ŁATA

Żądania HTTP PATCH mają na celu częściową aktualizację zasobu. Jeśli zobaczysz żądania PUT, zmodyfikuj również jednostkę zasobu, aby było jaśniej - metoda PATCH jest właściwym wyborem do częściowej aktualizacji istniejącego zasobu, a PUT powinna być używana tylko wtedy, gdy zastępujesz zasób w całości.

Więc powinieneś go używać w ten sposób:

POST    /device-management/devices      : Create a new device
PUT     /device-management/devices/{id} : Update the device information identified by "id"
PATCH   /device-management/devices/{id} : Partial-update the device information identified by "id"

Praktyki RESTful wskazują, że nie powinno mieć znaczenia, co umieścisz w / {id} - zawartość rekordu powinna zostać zaktualizowana do tej dostarczonej przez ładunek - ale GET / {id} powinien nadal prowadzić do tego samego zasobu.

Innymi słowy, PUT / 3 może aktualizować się do identyfikatora ładunku 4, ale GET / 3 powinien nadal łączyć się z tym samym ładunkiem (i zwracać ten z identyfikatorem ustawionym na 4).

Jeśli zdecydujesz, że twoje API wymaga tego samego identyfikatora w URI i ładunku, Twoim zadaniem jest upewnić się, że pasuje, ale zdecydowanie użyj PATCH zamiast PUT, jeśli wykluczasz identyfikator w ładunku, który powinien tam być w całości . W tym miejscu zaakceptowana odpowiedź jest błędna. PUT musi zastąpić cały zasób, gdzie poprawka-as może być częściowa.

CompEng88
źródło
1

Nie ma nic złego w stosowaniu różnych podejść. ale myślę, że najlepszym sposobem jest rozwiązanie z 2nd .

 PUT /person/UUID {"name": "Jimmy"}

 GET /person/UUID returns {"id": <UUID>, "name": "Jimmy"}

jest najczęściej używany w ten sposób, nawet struktura jednostek korzysta z tej techniki, gdy jednostka jest dodawana w dbContext klasa bez wygenerowanego identyfikatora jest identyfikatorem wygenerowanym przez odwołanie w Entity Framework.

Shan Khan
źródło
1

Patrzę na to z punktu widzenia JSON-LD / Semantic Web, ponieważ jest to dobry sposób na osiągnięcie prawdziwej zgodności REST, jak to opisałem na tych slajdach . Patrząc na to z tej perspektywy, nie ma wątpliwości, aby przejść do opcji (1.), ponieważ identyfikator (IRI) zasobu sieciowego powinien zawsze być równy adresowi URL, którego mogę użyć do wyszukania / wyłuskiwania zasobu. Myślę, że weryfikacja nie jest naprawdę trudna do wdrożenia ani nie jest intensywna obliczeniowo; więc nie uważam tego za ważny powód, aby wybrać opcję (2.). Myślę, że opcja (3.) tak naprawdę nie jest opcją, ponieważ POST (utwórz nowy) ma inną semantykę niż PUT (aktualizacja / zastąpienie).

vanthome
źródło
0

Może zajść potrzeba przyjrzenia się typom żądań PATCH / PUT.

Żądania PATCH są używane do częściowej aktualizacji zasobu, podczas gdy w żądaniach PUT musisz wysłać cały zasób, który zostanie nadpisany na serwerze.

Jeśli chodzi o posiadanie identyfikatora w adresie URL, myślę, że zawsze powinieneś go mieć, ponieważ identyfikacja zasobu jest standardową praktyką. Nawet API Stripe działa w ten sposób.

Możesz użyć żądania PATCH, aby zaktualizować zasób na serwerze o identyfikatorze, aby go zidentyfikować, ale nie aktualizuj rzeczywistego identyfikatora.

Noman Ur Rehman
źródło