Mam trzy pytania dotyczące projektu interfejsu API REST, które mam nadzieję, że ktoś może rzucić nieco światła. Szukałem bez końca od wielu godzin, ale nigdzie nie znalazłem odpowiedzi na moje pytania (może po prostu nie wiem, czego szukać?).
Pytanie 1
Moje pierwsze pytanie dotyczy działań / RPC. Od jakiegoś czasu opracowuję interfejs API REST i przywykłem do myślenia o kolekcjach i zasobach. Jednak natknąłem się na kilka przypadków, w których paradygmat wydaje się nie mieć zastosowania i zastanawiam się, czy istnieje sposób na pogodzenie tego z paradygmatem REST.
W szczególności mam przypadek, w którym modyfikacja zasobu powoduje wygenerowanie wiadomości e-mail. Jednak w późniejszym momencie użytkownik może wyraźnie wskazać, że chce ponownie wysłać wiadomość e-mail wysłaną wcześniej. Podczas ponownego wysyłania wiadomości e-mail żaden zasób nie jest modyfikowany. Żaden stan nie jest zmieniany. To po prostu działanie, które musi nastąpić. Działanie jest powiązane z określonym typem zasobu.
Czy właściwe jest mieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu (np. /collection/123?action=resendEmail
)? Czy lepiej byłoby określić akcję i przekazać identyfikator zasobu (np. /collection/resendEmail?id=123
)? Czy to niewłaściwy sposób na to? Tradycyjnie (przynajmniej z HTTP) wykonywana akcja jest metodą żądania (GET, POST, PUT, DELETE), ale tak naprawdę nie pozwalają na niestandardowe akcje z zasobem.
pytanie 2
Używam kwerendującego fragmentu adresu URL do filtrowania zestawu zasobów zwróconych podczas zapytania do kolekcji (np /collection?someField=someval
.). Następnie w moim kontrolerze API określam, jakie porównanie będzie miało miejsce z tym polem i wartością. Odkryłem, że to naprawdę nie działa. Potrzebuję sposobu, aby pozwolić użytkownikowi interfejsu API określić typ porównania, które chce wykonać.
Najlepszym pomysłem, jaki do tej pory wymyśliłem, jest zezwolenie użytkownikowi interfejsu API na określenie go jako dodatku do nazwy pola (np. /collection?someField:gte=someval
- aby wskazać, że powinien zwracać zasoby tam, gdzie someField
jest większe lub równe (> =) cokolwiek someval
jest Czy to dobry pomysł? Zły pomysł? Jeśli tak, to dlaczego? Czy istnieje lepszy sposób, aby umożliwić użytkownikowi określenie rodzaju porównania do wykonania z danym polem i wartością?
pytanie 3
Często widzę URI, które wyglądają jak /person/123/dogs
zdobycie person
s dogs
. Generalnie unikałem czegoś takiego, ponieważ na koniec sądzę, że tworząc taki identyfikator URI, uzyskujesz dostęp do dogs
kolekcji filtrowanej według określonego person
identyfikatora. Byłoby to równoważne z /dogs?person=123
. Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy ( /collection/resource_id
)?
Odpowiedzi:
Wolałbym modelować to w inny sposób, z kolekcją zasobów reprezentujących wiadomości e-mail, które mają zostać wysłane; wysyłanie zostanie przetworzone przez wewnętrzny serwis w odpowiednim czasie, w którym to momencie odpowiedni zasób zostanie usunięty. (Lub użytkownik może wcześniej usunąć zasób, powodując anulowanie żądania wysłania.)
Cokolwiek robisz, nie umieszczaj czasowników w nazwie zasobu! To rzeczownik (a część zapytania to zestaw przymiotników). Nouning czasowniki dziwne REST!
Wolę określić ogólną klauzulę filtrowania i mieć ją jako opcjonalny parametr zapytania przy każdym żądaniu pobrania zawartości kolekcji. Klient może następnie dokładnie określić, w jaki sposób ograniczyć zestaw zwracany, w dowolny sposób. Martwię się również trochę o możliwość wykrycia języka filtrów / zapytań; im bogatszy, tym trudniejszy do odkrycia dla dowolnych klientów. Alternatywnym podejściem, które, przynajmniej teoretycznie, zajmuje się tym zagadnieniem wykrywalności, jest umożliwienie tworzenia podrzędnych zasobów kolekcji, które klienci uzyskują przez POST dokumentu opisującego ograniczenie zasobu kolekcji. Nadal jest to niewielkie nadużycie, ale przynajmniej można je wyraźnie odkryć!
Tego rodzaju wykrywalność jest jedną z rzeczy, które uważam za najmniej silne przy REST.
Jeśli zagnieżdżona kolekcja jest naprawdę podfunkcją elementów członkowskich kolekcji zewnętrznej, uzasadnione jest, aby ustrukturyzować je jako pod-zasób. Przez „podfunkcję” rozumiem coś w rodzaju relacji składu UML, gdzie zniszczenie zewnętrznego zasobu naturalnie oznacza zniszczenie wewnętrznej kolekcji.
Inne typy kolekcji można modelować jako przekierowanie HTTP; dlatego
/person/123/dogs
rzeczywiście można na nie odpowiedzieć, wykonując 307, który przekierowuje do/dogs?person=123
. W tym przypadku kolekcja nie jest tak naprawdę kompozycją UML, ale agregacją UML. Różnica ma znaczenie; to jest znaczące!źródło
resendEmail
akcja mogłaby być obsadzona przez utworzenie kolekcji i wysłanie do niej, wydaje się to mniej naturalne. W rzeczywistości nie przechowuję niczego w bazie danych, gdy wiadomość e-mail jest wysyłana ponownie (bez potrzeby). Żaden zasób nie jest modyfikowany, dlatego jest to po prostu działanie, które się powiedzie lub nie. Nie mogłem zwrócić identyfikatora zasobu, który istnieje po zakończeniu połączenia, dzięki czemu taka implementacja stanie się hackiem, a nie RESTful. To po prostu nie jest operacja CRUD.Zrozumiałe jest, że jestem trochę zdezorientowany, jak prawidłowo używać REST w oparciu o wszystkie sposoby, w jakie widziałem, jak duże firmy projektują swoje API REST.
Masz rację, ponieważ REST to system gromadzenia zasobów. Oznacza Reprezentatywne przekazanie państwa. Niezbyt dobra definicja, jeśli mnie o to poprosisz. Ale głównymi pojęciami są 4 VERB HTTP i bycie bezpaństwowcami.
Ważną rzeczą do zapamiętania jest to, że masz tylko 4 CZASOWNIKI z RESZTĄ. Są to GET, POST, PUT i DELETE. Twoim
resend
przykładem byłoby dodanie nowego czasownika do REST. To powinna być czerwona flaga.Pytanie 1
Ważne jest, aby zdawać sobie sprawę, że osoba dzwoniąca do interfejsu API REST nie powinna wiedzieć, że wykonanie
PUT
w kolekcji spowoduje wygenerowanie wiadomości e-mail. To dla mnie pachnie wyciekiem. Mogli wiedzieć, że wykonanie aPUT
może spowodować dodatkowe zadania, które mogą później wykonać zapytanie. Wiedzieliby to, wykonującGET
niedawno utworzony zasób. ToGET
zwróci zasób i wszystkieTask
powiązane z nim identyfikatory zasobów. Następnie możesz później wysłać zapytanie do tych zadań, aby ustalić ich status, a nawet przesłać noweTask
.Masz kilka opcji.
REST - podejście oparte na zasobach zadania
Utwórz
tasks
zasób, w którym możesz przesyłać określone zadania do systemu w celu wykonywania działań. Możesz następnieGET
wykonać zadanie na podstawieID
zwróconego zadania , aby ustalić jego status.Możesz też połączyć
SOAP over HTTP
usługę internetową, aby dodać RPC do swojej architektury.zapytania do wszystkich zadań dla określonego zasobu
GET http://server/api/myCollection/123/tasks
przykład zasobu zadania
PUT http://server/api/tasks
==> zwraca identyfikator zadania
223334
GET http://server/api/tasks/223334
REST - używanie POST do wyzwalania akcji
Zawsze możesz dodać
POST
dodatkowe dane do zasobu. Moim zdaniem naruszyłoby to ducha REST, ale nadal byłoby zgodne.Możesz wykonać test POST podobny do tego:
POST http://server/api/collection/123
{ "action" : "send-email" }
Będziesz aktualizować zasób 123 z kolekcji o dodatkowe dane. Te dodatkowe dane są w zasadzie działaniem nakazującym backendowi wysłanie wiadomości e-mail dla tego zasobu.
Mam z tym problem, że a
GET
w zasobie zwróci te zaktualizowane dane. Jednak rozwiązałoby to Twoje wymagania i nadal byłoby ODPOWIEDNIE.SOAP - usługa sieciowa, która akceptuje zasoby uzyskane z REST
Utwórz nową usługę WebService, w której możesz wysyłać wiadomości e-mail na podstawie poprzedniego identyfikatora zasobu z interfejsu API REST. Nie będę tutaj szczegółowo omawiał SOAP, ponieważ pierwotne pytanie dotyczy REST, a tych dwóch koncepcji / technologii nie należy porównywać, ponieważ są to jabłka i pomarańcze .
pytanie 2
Masz również kilka opcji tutaj:
Wydaje się, że wiele większych firm publikujących API REST udostępnia
search
kolekcję, która jest tak naprawdę tylko sposobem na przekazanie parametrów zapytania w celu zwrócenia zasobów.GET http://server/api/search?q="type = myCollection & someField >= someval"
Które zwróciłyby kolekcję w pełni kwalifikowanych zasobów REST, takich jak:
Lub możesz zezwolić na coś takiego jak MVEL jako parametr zapytania.
pytanie 3
Wolę pod-poziomy niż konieczność powrotu do poprzedniej wersji i zapytania do innego zasobu za pomocą parametru zapytania. Nie wierzę, że istnieje jakakolwiek zasada w taki czy inny sposób. Możesz zaimplementować oba sposoby i pozwolić dzwoniącemu zdecydować, który jest bardziej odpowiedni na podstawie tego, jak po raz pierwszy wszedł do systemu.
Notatki
Nie zgadzam się co do czytelności komentarzy innych osób. Pomimo tego, co niektórzy mogą sądzić, REST wciąż nie jest przeznaczony do spożycia przez ludzi. Przeznaczony jest na zużycie maszynowe. Jeśli chcę zobaczyć moje tweety, korzystam ze zwykłej strony internetowej Twittera. Nie wykonuję REST GET z ich API. Jeśli chcę programowo zrobić coś z moimi tweetami, używam ich interfejsu API REST. Tak, interfejsy API powinny być zrozumiałe, ale
gte
nie jest tak źle, po prostu nie jest intuicyjne.Inną najważniejszą rzeczą w REST jest to, że powinieneś być w stanie rozpocząć w dowolnym punkcie API i przejść do wszystkich innych powiązanych zasobów BEZ znajomości dokładnego adresu URL innych zasobów z wyprzedzeniem. Wyniki
GET
VERB w REST powinny zwrócić pełny adres URL REST zasobów, do których się odwołuje. Zamiast zapytania zwracającego identyfikatorPerson
obiektu zwróciłby w pełni kwalifikowany adres URL, taki jakhttp://server/api/people/13
. Następnie zawsze możesz programowo nawigować po wynikach, nawet jeśli zmieni się adres URL.Odpowiedź na komentarz
W prawdziwym świecie rzeczy, które muszą się wydarzyć, nie tworzą, nie czytają, nie aktualizują ani nie usuwają zasobów (CRUD).
Można podjąć dodatkowe działania dotyczące zasobów. Typowe relacyjne bazy danych obsługują koncepcję procedur przechowywanych. Są to dodatkowe polecenia, które można wykonać na zestawie danych. REST z natury nie ma tej koncepcji. I nie ma powodu, by tak było. Tego typu akcje są idealne dla RPC lub SOAP Web Services.
Jest to ogólny problem, który widzę z interfejsami API REST. Deweloperzy nie lubią ograniczeń koncepcyjnych otaczających REST, więc dostosowują go do robienia tego, co chcą. To jednak łamie go od bycia usługą RESTful. Zasadniczo te adresy URL stają się
GET
wywołaniami pseudo-REST-serwletów.Masz kilka opcji:
POST
dodatkowych danych do zasobu w celu wykonania akcji.Jeśli użyjesz parametru zapytania, którego VERB HTTP użyjesz do ponownego wysłania wiadomości e-mail?
GET
- Czy to ponownie wysyła wiadomość e-mail ORAZ zwraca dane zasobu? Co jeśli system buforował ten adres URL i traktował go jak unikalny adres URL tego zasobu. Za każdym razem, gdy trafią w adres URL, ponownie wysyła wiadomość e-mail.POST
- W rzeczywistości nie wysłałeś żadnych nowych danych do zasobu, tylko dodatkowy parametr zapytania.W oparciu o wszystkie podane wymagania, wykonanie
POST
na zasobie zaction field
danymi POST rozwiąże problem.źródło
Pytanie 1: Czy właściwe jest zmieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu [lub] czy lepiej byłoby określić akcję i przekazać identyfikator zasobu?
Dobre pytanie. W takim przypadku radzę zastosować to drugie podejście, a mianowicie określić akcję i przekazać identyfikator zasobu. W ten sposób, gdy twój zasób jest po raz pierwszy modyfikowany, z kolei wywołuje
/sendEmail
akcję (uwaga: nie trzeba nazywać go „ponownie wyślij”) jako osobne żądanie RESTful (które możesz później wywoływać wielokrotnie, niezależnie od modyfikowanego zasobu ).Pytanie 2: dotyczące używania operatora porównania takiego:
/collection?someField:gte=someval
Chociaż jest to technicznie w porządku, prawdopodobnie jest to zły pomysł. Jedną z kluczowych zasad REST jest czytelność. Sugerowałbym, aby po prostu przekazać operator porównania jako inny parametr, na przykład:
/collection?someField=someval&operator=gte
i oczywiście zaprojektować swój interfejs API, aby obsługiwał domyślny przypadek (w przypadku, gdyoperator
parametr zostanie pominięty w URI).Pytanie 3: Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy?
Tak; dla abstrakcji. Widziałem kilka interfejsów API REST, które wykorzystują warstwy abstrakcji na wielu poziomach URI, na przykład:
/vehicles/cars/123
lub/vehicles/bikes/123
które z kolei umożliwiają pracę z przydatnymi informacjami dotyczącymi zarówno kolekcji, jak/vehicles
i/vehicles/bikes
kolekcji. Powiedziawszy to, nie jestem wielkim fanem tego podejścia; rzadko musisz to robić w praktyce, a istnieje szansa, że możesz przeprojektować interfejs API, aby używał tylko 2 poziomów.I tak, jak sugerują powyższe komentarze, w przyszłości najlepiej byłoby podzielić pytania na osobne posty;)
źródło
/collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq
.W przypadku pytania 2 inna alternatywa może być bardziej elastyczna: rozważ każde wyszukiwanie jako zasób, który użytkownik buduje przed użyciem.
powiedzmy, że masz kontener „wyszukiwań”, tam robisz
POST /api/searches/
ze specyfikacją zapytania dla treści. może to być dokument JSON, XML lub nawet SQL, cokolwiek jest dla Ciebie łatwiejsze. Jeśli zapytanie zostanie poprawnie przeanalizowane, nowe wyszukiwanie zostanie utworzone jako nowy zasób z własnym identyfikatorem URI, powiedzmy/api/searches/q123/
Następnie klient może po prostu
GET /api/searches/q123/
pobrać wyniki zapytania.Na koniec możesz poprosić klienta o usunięcie zapytania lub wyczyszczenie go po zamknięciu sesji.
źródło
Nie, nie jest to właściwe, ponieważ IRI służą do identyfikowania zasobów, a nie operacji (jednak ppl używa przez pewien czas metody zastępowania tej metody , w przypadkach gdy użycie metod innych niż POST i GET nie jest obsługiwane). Możesz poszukać odpowiedniej metody HTTP lub utworzyć nową. W takich przypadkach POST może być twoim przyjacielem (ppl użyj go, jeśli nie mogą znaleźć odpowiedniej metody, a żądanie nie jest pobierane). Inne podejście do tworzenia zasobów z wysyłania wiadomości e-mail, dzięki czemu
POST /emails
można wysyłać wiadomości e-mail bez tworzenia prawdziwych zasobów. Btw. Struktura URI nie zawiera semantyki, więc z perspektywy REST tak naprawdę nie ma znaczenia, jakiego rodzaju URI używasz. Liczy się metadane (np. Relacja linków ) przypisane do linków wysłanych do klientów.Nie musisz tworzyć własnego języka zapytań. Wolę użyć już istniejącego i dodać opis zapytania do metadanych łącza. W tym celu należy użyć prawdopodobnie typu nośnika RDF (np. JSON-LD) lub niestandardowego typu MIME (jeśli nie ma formatu obsługującego ten format inny niż RDF). Korzystanie z istniejących standardów oddziela klienta od serwera, na tym polega jednolite ograniczenie interfejsu.
Jak już wspomniałem, struktura URI nie ma znaczenia z perspektywy REST. Możesz użyć
/x71fd823df2
na przykład. Nadal miałoby to sens dla klientów, ponieważ sprawdzają metadane przypisane do łączy, a nie strukturę URI. Głównym celem identyfikatora URI jest identyfikacja zasobów. W standardzie URI stwierdzają, że ścieżka zawiera dane hierarchiczne, a zapytanie zawiera dane niehierarchiczne. Ale to, co jest hierarchiczne, może być bardzo subiektywne. Dlatego też możesz napotkać wiele poziomów głębokich identyfikatorów URI i identyfikatorów URI z długimi zapytaniami.Powinieneś przeczytać przynajmniej ograniczenia REST z rozprawy Fielding , standardu HTTP i prawdopodobnie internetowych interfejsów API trzeciej generacji od Markusa.
źródło