Najlepsza praktyka dla częściowych aktualizacji w usłudze RESTful

208

Piszę usługę RESTful dla systemu zarządzania klientami i próbuję znaleźć najlepszą praktykę częściowej aktualizacji rekordów. Na przykład chcę, aby osoba dzwoniąca mogła odczytać pełny rekord za pomocą żądania GET. Ale do jego aktualizacji dozwolone są tylko niektóre operacje na rekordzie, takie jak zmiana stanu z WŁĄCZONEGO na WYŁĄCZONY. (Mam bardziej złożone scenariusze niż to)

Nie chcę, aby osoba dzwoniąca przesłała cały rekord ze zaktualizowanym polem ze względów bezpieczeństwa (wydaje się to również przesadą).

Czy istnieje zalecany sposób konstruowania identyfikatorów URI? Czytając książki REST, wywołania w stylu RPC wydają się być niezadowolone.

Jeśli poniższe wywołanie zwróci pełny rekord klienta dla klienta o identyfikatorze 123

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>

jak powinienem zaktualizować status?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Aktualizacja : Aby rozszerzyć pytanie. W jaki sposób włącza się „wywołania logiki biznesowej” do interfejsu API REST? Czy istnieje uzgodniony sposób na zrobienie tego? Nie wszystkie metody są z natury CRUD. Niektóre są bardziej złożone, na przykład „ sendEmailToCustomer (123) ”, „ mergeCustomers (123, 456) ”, „ countCustomers ()

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 
magiconair
źródło
3
Aby odpowiedzieć na twoje pytanie dotyczące „połączeń z logiką biznesową”, oto post POSTod samego Roy'a Fieldinga: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post gdzie podstawową ideą jest: jeśli nie ma to metoda (taka jak GETlub PUT) idealnie dopasowana do twojego zastosowania operacyjnego POST.
rojoca
To właśnie robiłem. Wykonuj połączenia REST w celu pobierania i aktualizowania znanych zasobów za pomocą GET, PUT, DELETE. POST do dodawania nowych zasobów i POST z jakimś opisowym adresem URL dla wywołań logiki biznesowej.
magiconair
Cokolwiek wybierzesz, jeśli ta operacja nie jest częścią odpowiedzi GET, nie masz usługi RESTful. Nie widzę tego tutaj
MStodd,

Odpowiedzi:

69

Zasadniczo masz dwie opcje:

  1. Użyj PATCH(ale pamiętaj, że musisz zdefiniować własny typ nośnika, który określa, co dokładnie się stanie)

  2. Użyj POSTdo zasobu podrzędnego i zwróć 303 Zobacz Inne z nagłówkiem Lokalizacja wskazującym na główny zasób. Zamiarem 303 jest powiedzenie klientowi: „Przeprowadziłem test POST i efektem było zaktualizowanie innego zasobu. Zobacz nagłówek lokalizacji, dla którego to było”. POST / 303 jest przeznaczony do iteracyjnych dodatków do zasobów w celu zbudowania stanu niektórych głównych zasobów i doskonale nadaje się do częściowych aktualizacji.

Jan Algermissen
źródło
OK, POST / 303 ma dla mnie sens. PATCH i MERGE Nie mogłem znaleźć na liście prawidłowych czasowników HTTP, więc wymagałoby to więcej testów. Jak zbudować identyfikator URI, jeśli chcę, aby system wysłał wiadomość e-mail do klienta 123? Coś w rodzaju czystego wywołania metody RPC, które w ogóle nie zmienia stanu obiektu. Jaki jest RESTful to zrobić?
magiconair
Nie rozumiem pytania o adres e-mail URI. Czy chcesz wdrożyć bramę, którą możesz POST, aby wysłać e-mail, czy szukasz mailto: [email protected]?
Jan Algermissen
15
Ani REST, ani HTTP nie mają nic wspólnego z CRUD, poza niektórymi ludźmi utożsamiającymi metody HTTP z CRUD. REST polega na manipulowaniu stanem zasobów poprzez przenoszenie reprezentacji. Cokolwiek chcesz osiągnąć, przenieś reprezentację do zasobu z odpowiednią semantyką. Uważaj na pojęcia „czysta metoda wywołań” lub „logika biznesowa”, ponieważ zbyt łatwo sugerują, że „HTTP służy do transportu”. Jeśli musisz wysłać wiadomość e-mail, POST do zasobu bramy, jeśli chcesz połączyć się z kontami, utwórz nowe i reprezentacje POST pozostałych dwóch itd.
Jan Algermissen
9
Zobacz także, jak Google to robi: googlecode.blogspot.com/2010/03/…
Marius
4
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{„op”: „test”, „ścieżka”: „/ a / b / c”, „wartość” : „foo”}, {„op”: „remove”, „path”: „/ a / b / c”}, {„op”: „add”, „path”: „/ a / b / c” , „value”: [„foo”, „bar”]}, {„op”: „replace”, „path”: „/ a / b / c”, „value”: 42}, {„op”: „move”, „from”: „/ a / b / c”, „path”: „/ a / b / d”}, {„op”: „copy”, „from”: „/ a / b / d "," path ":" / a / b / e "}]
intotecho
48

Powinieneś używać POST do częściowych aktualizacji.

Aby zaktualizować pola dla klienta 123, wykonaj test POST na / customer / 123.

Jeśli chcesz zaktualizować tylko status, możesz również PUT ustawić na / customer / 123 / status.

Zasadniczo żądania GET nie powinny mieć żadnych skutków ubocznych, a PUT służy do zapisywania / zastępowania całego zasobu.

Wynika to bezpośrednio z HTTP, jak widać tutaj: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods

wsorenson
źródło
1
@John Saunders POST nie musi koniecznie tworzyć nowego zasobu, który jest dostępny z URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson
10
@wsorensen: Wiem, że nie musi to prowadzić do nowego adresu URL, ale nadal uważałem, że test POST /customer/123powinien stworzyć oczywistą rzecz, która logicznie jest pod klientem 123. Może zamówienie? /customer/123/statusWydaje się, że PUT ma lepszy sens, zakładając, że test POST /customersdomyślnie utworzył status(i zakładając, że jest to legalny REST).
John Saunders
1
@John Saunders: praktycznie mówiąc, jeśli chcemy zaktualizować pole w zasobie znajdującym się przy danym URI, POST ma więcej sensu niż PUT, a przy braku AKTUALIZACJI uważam, że jest często używany w usługach REST. POST dla / klientów może utworzyć nowego klienta, a status PUT dla / customer / 123 / może lepiej zgadzać się ze słowem specyfikacji, ale jeśli chodzi o najlepsze praktyki, nie sądzę, aby istniał jakiś powód, aby nie przesyłać POST do / customer / 123, aby zaktualizować pole - jest zwięzłe, ma sens i nie jest sprzeczne z niczym w specyfikacji.
wsorenson
8
Czy żądania POST nie powinny być idempotentne? Z pewnością aktualizacja wpisu jest idempotentna i dlatego powinna być PUT?
Martin Andersson,
1
@MartinAndersson - żądania POSTnie muszą być idempotentne. I jak wspomniano, PUTmusi zastąpić cały zasób.
Halle Knast
10

Powinieneś używać PATCH do częściowych aktualizacji - albo używając dokumentów łatki json (patrz http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 lub http://www.mnot.net/ blog / 2012/09/05 / patch ) lub framework łatek XML (patrz http://tools.ietf.org/html/rfc5261 ). Moim zdaniem json-patch najlepiej pasuje do twoich danych biznesowych.

PATCH z dokumentami łatek JSON / XML ma bardzo wyraźną semantykę do przodu dla częściowych aktualizacji. Jeśli zaczniesz używać POST ze zmodyfikowanymi kopiami oryginalnego dokumentu, w przypadku częściowych aktualizacji wkrótce napotkasz problemy, w których chcesz, aby brakujące wartości (lub raczej wartości zerowe) reprezentowały albo „zignoruj ​​tę właściwość”, albo „ustaw tę właściwość na pusta wartość ”- a to prowadzi do króliczej dziury zhakowanych rozwiązań, które ostatecznie doprowadzą do stworzenia własnego formatu łaty.

Bardziej szczegółową odpowiedź znajdziesz tutaj: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .

Jørn Wildt
źródło
Należy pamiętać, że tymczasem RFC dla JSON-łaty i xml-łaty zostały sfinalizowane.
botchniaque
8

Mam podobny problem. Wydaje się, że PUT dla zasobu podrzędnego działa, gdy chcesz zaktualizować tylko jedno pole. Czasami jednak chcesz zaktualizować kilka rzeczy: Pomyśl o formularzu internetowym reprezentującym zasób z opcją zmiany niektórych wpisów. Przesłanie formularza przez użytkownika nie powinno skutkować wieloma PUT.

Oto dwa rozwiązania, o których mogę myśleć:

  1. wykonaj PUT z całym zasobem. Po stronie serwera zdefiniuj semantykę, że PUT z całym zasobem ignoruje wszystkie wartości, które nie uległy zmianie.

  2. zrobić PUT z częściowym zasobem. Po stronie serwera zdefiniuj semantykę tego połączenia.

2 jest tylko optymalizacją przepustowości wynoszącą 1. Czasami 1 jest jedyną opcją, jeśli zasób definiuje, że niektóre pola są polami wymaganymi (pomyślmy o buforach proto).

Problemem obu tych podejść jest oczyszczenie pola. Będziesz musiał zdefiniować specjalną wartość zerową (szczególnie dla buforów proto, ponieważ wartości zerowe nie są zdefiniowane dla buforów proto), co spowoduje wyczyszczenie pola.

Komentarze?

użytkownik360657
źródło
2
Byłoby to bardziej przydatne, gdyby zostało opublikowane jako osobne pytanie.
intotecho,
6

Myślę, że do modyfikowania statusu podejście RESTful polega na użyciu logicznego pod-zasobu, który opisuje status zasobów. To IMO jest bardzo przydatne i czyste, gdy masz ograniczony zestaw statusów. Sprawia, że ​​interfejs API jest bardziej wyrazisty bez wymuszania istniejących operacji na zasobach klienta.

Przykład:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}

Usługa POST powinna zwrócić nowo utworzonego klienta o identyfikatorze:

{
    id:123,
    ...  // the other fields here
}

GET dla utworzonego zasobu użyłby lokalizacji zasobu:

GET /customer/123/active

GET / klient / 123 / nieaktywny powinien zwrócić 404

W przypadku operacji PUT bez podania obiektu Json po prostu zaktualizuje status

PUT /customer/123/inactive  <-- Deactivating an existing customer

Podanie podmiotu pozwoli ci zaktualizować zawartość klienta i jednocześnie zaktualizować status.

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}

Tworzysz koncepcyjny pod-zasób dla zasobu klienta. Jest to również zgodne z definicją zasobu Roya Fieldinga: „... Zasób jest koncepcyjnym odwzorowaniem na zbiór bytów, a nie byt, który odpowiada odwzorowaniu w dowolnym momencie w czasie ...” W tym przypadku mapowanie koncepcyjne jest aktywne-klient do klienta ze statusem = AKTYWNY.

Przeczytaj operację:

GET /customer/123/active 
GET /customer/123/inactive

Jeśli wykonujesz te połączenia jedna po drugiej, druga z nich musi zwrócić status 404, pomyślne wyjście może nie obejmować statusu, ponieważ jest niejawny. Oczywiście nadal możesz użyć GET / customer / 123? Status = AKTYWNY | NIEAKTYWNY do bezpośredniego zapytania do zasobu klienta.

Operacja DELETE jest interesująca, ponieważ semantyka może być myląca. Możesz jednak nie opublikować tej operacji dla tego zasobu koncepcyjnego lub użyć go zgodnie z logiką biznesową.

DELETE /customer/123/active

Ten może doprowadzić klienta do statusu USUŃ / WYŁĄCZONY lub do statusu przeciwnego (AKTYWNY / NIEAKTYWNY).

raspacorp
źródło
Jak dostać się do pod-zasobu?
MStodd,
Odpowiedziałem ponownie, próbując wyjaśnić
raspacorp,
5

Rzeczy, które należy dodać do rozszerzonego pytania. Myślę, że często możesz doskonale zaprojektować bardziej skomplikowane działania biznesowe. Ale musisz podać sposób myślenia w stylu metody / procedury i więcej myśleć w zasobach i czasownikach.

wysyłki pocztowe


POST /customers/123/mails

payload:
{from: [email protected], subject: "foo", to: [email protected]}

Wdrożenie tego zasobu + POST wyśle ​​następnie pocztę. jeśli to konieczne, możesz zaoferować coś takiego jak / customer / 123 / outbox, a następnie zaoferować łącza do zasobów do / customer / mails / {mailId}.

liczba klientów

Możesz sobie z tym poradzić jak zasobem wyszukiwania (w tym metadanymi wyszukiwania z stronicowaniem i informacjami o liczbie znalezionych, co daje liczbę klientów).


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}

Manuel Aldana
źródło
Podoba mi się sposób logicznego grupowania pól w podźródle POST.
gertas
3

Użyj PUT do aktualizacji niekompletnych / częściowych zasobów.

Możesz zaakceptować jObject jako parametr i przeanalizować jego wartość, aby zaktualizować zasób.

Poniżej znajduje się funkcja, której można użyć jako odniesienia:

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    foreach (JProperty property in json.Properties())
    {
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    }

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);            
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);
}
Puneet Pathak
źródło
2

Jeśli chodzi o twoją aktualizację.

Uważam, że koncepcja CRUD spowodowała pewne zamieszanie w zakresie projektowania API. CRUD to ogólna koncepcja niskiego poziomu dla podstawowych operacji wykonywanych na danych, a czasowniki HTTP to tylko metody żądania ( utworzone 21 lat temu ), które mogą, ale nie muszą, być mapowane na operację CRUD. W rzeczywistości spróbuj znaleźć obecność akronimu CRUD w specyfikacji HTTP 1.0 / 1.1.

Bardzo dobrze wyjaśniony przewodnik, który stosuje konwencję pragmatyczną, można znaleźć w dokumentacji interfejsu API platformy chmurowej Google . Opisuje koncepcje tworzenia interfejsu API opartego na zasobach, który kładzie duży nacisk na zasoby w porównaniu z operacjami, a także opisywane przypadki użycia. Chociaż jest to konwencyjny projekt ich produktu, myślę, że ma to sens.

Podstawową koncepcją tutaj (i taką, która powoduje wiele nieporozumień) jest mapowanie między „metodami” i czasownikami HTTP. Jedną rzeczą jest zdefiniowanie, jakie „operacje” (metody) będzie wykonywał Twój interfejs API w odniesieniu do rodzajów zasobów (na przykład uzyskać listę klientów lub wysłać wiadomość e-mail), a drugą są czasowniki HTTP. Musi istnieć definicja obu metod i czasowników, których zamierzasz użyć, oraz mapowanie między nimi .

Mówi też, że gdy operacja nie mapuje dokładnie ze standardową metodą ( List, Get, Create, Update, Deletew tym przypadku), jeden może korzystać z metody „niestandardowe”, jak BatchGet, który pobiera kilka obiektów na podstawie kilku wejścia ID obiektu, lub SendEmail.

atoledo
źródło
2

RFC 7396 : Poprawka scalająca JSON (opublikowana cztery lata po opublikowaniu pytania) opisuje najlepsze praktyki dla PATCH pod względem formatu i reguł przetwarzania.

W skrócie, przesyłasz PATCH HTTP do zasobu docelowego z typem aplikacji / merge-patch + json MIME i treści reprezentujących tylko te części, które chcesz zmienić / dodać / usunąć, a następnie postępować zgodnie z poniższymi regułami przetwarzania.

Zasady :

  • Jeśli podana poprawka scalająca zawiera elementy, które nie pojawiają się w elemencie docelowym, elementy te są dodawane.

  • Jeśli cel zawiera element członkowski, wartość zostanie zastąpiona.

  • Wartości zerowe w łatce scalającej mają specjalne znaczenie, aby wskazać usunięcie istniejących wartości w celu.

Przykładowe przypadki testowe, które ilustrują powyższe reguły (jak widać w dodatku do tego dokumentu RFC):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}
Voicu
źródło
1

Sprawdź http://www.odata.org/

Definiuje metodę MERGE, więc w twoim przypadku byłoby to mniej więcej tak:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Tylko statuswłaściwość jest aktualizowana, a pozostałe wartości są zachowywane.

Max Toro
źródło
Czy MERGEprawidłowy czasownik HTTP?
John Saunders
3
Spójrz na PATCH - który wkrótce będzie standardowym HTTP i robi to samo.
Jan Algermissen
@John Saunders Tak, to metoda rozszerzenia.
Max Toro
FYI MERGE zostało usunięte z OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Zobacz docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k,
0

To nie ma znaczenia Jeśli chodzi o REST, nie możesz wykonać GET, ponieważ nie jest buforowalny, ale nie ma znaczenia, czy używasz POST, PATCH, PUT itp. I nie ma znaczenia, jak wygląda adres URL. Jeśli wykonujesz REST, ważne jest to, że gdy otrzymasz reprezentację twojego zasobu z serwera, reprezentacja ta może dać opcje przejścia do stanu klienta.

Jeśli Twoja odpowiedź GET zawierała zmiany stanu, klient musi tylko wiedzieć, jak je odczytać, a serwer może je zmienić w razie potrzeby. Tutaj aktualizacja jest wykonywana przy użyciu POST, ale jeśli zmieniono ją na PATCH lub jeśli zmieni się adres URL, klient nadal wie, jak dokonać aktualizacji:

{
  "customer" :
  {
  },
  "operations":
  [
    "update" : 
    {
      "method": "POST",
      "href": "https://server/customer/123/"
    }]
}

Możesz posunąć się do wykazania wymaganych / opcjonalnych parametrów, które klient może Ci zwrócić. To zależy od zastosowania.

Jeśli chodzi o operacje biznesowe, może to być inny zasób powiązany z zasobem klienta. Jeśli chcesz wysłać wiadomość e-mail do klienta, być może ta usługa jest jego własnym zasobem, do którego możesz wysłać POST, więc możesz dołączyć następującą operację do zasobu klienta:

"email":
{
  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"
}

Oto kilka dobrych filmów i przykład architektury REST prezentera. Stormpath używa tylko GET / POST / DELETE, co jest w porządku, ponieważ REST nie ma nic wspólnego z używanymi operacjami ani z tym, jak powinny wyglądać adresy URL (oprócz GET powinny być buforowalne):

https://www.youtube.com/watch?v=pspy1H6A3FM ,
https://www.youtube.com/watch?v=5WXYw4J4QOU ,
http://docs.stormpath.com/rest/quickstart/

MStodd
źródło