Jaka jest najlepsza metoda RESTful zwracająca całkowitą liczbę elementów w obiekcie?

139

Tworzę usługę REST API dla dużego serwisu społecznościowego, w którym jestem zaangażowany. Na razie działa świetnie. Mogę wydać GET, POST, PUTi DELETEwnioski do adresów URL obiektu i wpływają na moje dane. Jednak te dane są stronicowane (ograniczone do 30 wyników na raz).

Jaki byłby jednak najlepszy sposób RESTful na uzyskanie całkowitej liczby powiedzmy członków za pośrednictwem mojego interfejsu API?

Obecnie wysyłam żądania do struktury adresu URL, takiej jak:

  • / api / members - zwraca listę członków (30 naraz, jak wspomniano powyżej)
  • / api / Members / 1 - wpływa na pojedynczego członka, w zależności od użytej metody żądania

Moje pytanie brzmi: w jaki sposób użyłbym podobnej struktury adresu URL, aby uzyskać całkowitą liczbę członków w mojej aplikacji? Oczywiście żądanie tylko idpola (podobnie jak w Graph API Facebooka) i zliczanie wyników byłoby nieskuteczne, biorąc pod uwagę, że zwrócony zostałby tylko wycinek 30 wyników.

Martin Bean
źródło

Odpowiedzi:

84

Podczas gdy odpowiedź do / API / users jest stronicowana i zwraca tylko 30 rekordów, nic nie stoi na przeszkodzie, aby w odpowiedzi uwzględnić również całkowitą liczbę rekordów i inne istotne informacje, takie jak rozmiar strony, numer strony / przesunięcie itp. .

Interfejs API StackOverflow jest dobrym przykładem tego samego projektu. Oto dokumentacja dotycząca metody Users - https://api.stackexchange.com/docs/users

Franci Penov
źródło
3
+1: Zdecydowanie najbardziej RESTful rzecz do zrobienia, jeśli w ogóle zostaną nałożone limity pobierania.
Donal Fellows
2
@bzim Wiedziałbyś, że jest następna strona do pobrania, ponieważ istnieje link z rel = "next".
Darrel Miller
4
@Donal „następny” rel jest zarejestrowany w IANA iana.org/ assignments
Darrel Miller
1
@Darrel - tak, można to zrobić z dowolnym rodzajem flagi „next” w ładunku. Po prostu czuję, że posiadanie całkowitej liczby elementów kolekcji w odpowiedzi jest cenne samo w sobie i działa jak flaga „następny”.
Franci Penov,
5
Zwrócenie obiektu, który nie jest listą pozycji, nie jest właściwą implementacją REST API, ale REST nie zapewnia żadnego sposobu na uzyskanie częściowej listy wyników. Aby to uszanować, myślę, że powinniśmy używać nagłówków do przesyłania innych informacji, takich jak suma, token następnej strony i token poprzedniej strony. Nigdy tego nie próbowałem i potrzebuję porady od innych programistów.
Loenix
74

Wolę używać nagłówków HTTP do tego rodzaju informacji kontekstowych.

Dla całkowitej liczby elementów używam X-total-countnagłówka.
W przypadku linków do następnej, poprzedniej strony itp. Używam Linknagłówka http :
http://www.w3.org/wiki/LinkHeader

Github robi to w ten sam sposób: https://developer.github.com/v3/#pagination

Moim zdaniem jest bardziej przejrzysty, ponieważ można go używać również przy zwracaniu treści, które nie obsługują hiperłączy (np. Pliki binarne, obrazy).

Ondrej Bozek
źródło
5
RFC6648 wycofuje konwencję poprzedzania nazw niestandardowych parametrów ciągiem znaków X-.
JDawg,
70

Ostatnio przeprowadziłem obszerne badania nad tym i innymi pytaniami związanymi ze stronicowaniem REST i pomyślałem, że dodanie niektórych moich ustaleń tutaj jest konstruktywne. Rozszerzam nieco pytanie, aby uwzględnić przemyślenia na temat stronicowania, a także liczbę, ponieważ są one ściśle powiązane.

Nagłówki

Metadane stronicowania są dołączane do odpowiedzi w postaci nagłówków odpowiedzi. Dużą zaletą tego podejścia jest to, że sam ładunek odpowiedzi jest po prostu tym, o co prosił żądający danych. Ułatwienie przetwarzania odpowiedzi klientom, którzy nie są zainteresowani informacjami dotyczącymi stronicowania.

Istnieje wiele (standardowych i niestandardowych) nagłówków używanych w środowisku naturalnym do zwracania informacji związanych ze stronicowaniem, w tym całkowitej liczby.

Całkowita liczba X

X-Total-Count: 234

Jest to używane w niektórych interfejsach API , które znalazłem na wolności. Istnieją również pakiety NPM do dodania obsługi tego nagłówka np. Do Loopback. Niektóre artykuły zalecają również ustawienie tego nagłówka.

Jest często używany w połączeniu z Linknagłówkiem, co jest całkiem dobrym rozwiązaniem do stronicowania, ale brakuje mu informacji o całkowitej liczbie.

Połączyć

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Czuję, czytając wiele na ten temat, że ogólny konsensus jest użycie Linknagłówka , aby zapewnić stronicowania linki do klientów korzystających rel=next, rel=previousitd. Problemem jest to, że brakuje informacji o tym, jak wiele łącznych zapisów istnieją, co jest dlaczego wiele interfejsów API łączy to z X-Total-Countnagłówkiem.

Alternatywnie, niektóre API, np. Standard JsonApi , używają Linkformatu, ale dodają informacje w kopercie odpowiedzi zamiast do nagłówka. Upraszcza to dostęp do metadanych (i tworzy miejsce do dodawania informacji o całkowitym zliczeniu) kosztem rosnącej złożoności dostępu do samych danych (poprzez dodanie koperty).

Zakres zawartości

Content-Range: items 0-49/234

Promowany przez artykuł na blogu zatytułowany Nagłówek zakresu, wybieram Cię (do paginacji)! . Autor przedstawia argumenty przemawiające za używaniem nagłówków Rangei Content-Rangedo paginacji. Kiedy uważnie zapoznać się z RFC na te nagłówki, okazuje się, że rozszerzenie ich znaczenie poza zakresy bajtów faktycznie zablokowane przez RFC i jest wyraźnie dozwolone. Użyty w kontekście itemszamiast bytes, nagłówek Range w rzeczywistości umożliwia nam zarówno zażądanie określonego zakresu elementów, jak i wskazanie, do jakiego zakresu całkowitego wyniku odnoszą się elementy odpowiedzi. Ten nagłówek daje również świetny sposób na pokazanie całkowitej liczby. I jest to prawdziwy standard, który przeważnie odwzorowuje stronicowanie jeden do jednego. Jest również używany na wolności .

Koperta

Wiele interfejsów API, w tym ten z naszej ulubionej witryny z pytaniami i odpowiedziami, wykorzystuje kopertę , otokę wokół danych, która służy do dodawania metainformacji o danych. Ponadto standardy OData i JsonApi używają obwiedni odpowiedzi.

Dużą wadą tego (imho) jest to, że przetwarzanie danych odpowiedzi staje się bardziej złożone, ponieważ rzeczywiste dane muszą znajdować się gdzieś w kopercie. Istnieje również wiele różnych formatów tej koperty i musisz użyć właściwego. Jest to wymowne, że obwiednie odpowiedzi z OData i JsonApi są bardzo różne, z mieszaniem OData w metadanych w wielu punktach odpowiedzi.

Oddzielny punkt końcowy

Myślę, że zostało to wystarczająco uwzględnione w innych odpowiedziach. Nie badałem tak dużo, ponieważ zgadzam się z komentarzami, że jest to mylące, ponieważ masz teraz wiele typów punktów końcowych. Myślę, że najładniej jest, jeśli każdy punkt końcowy reprezentuje (zbiór) zasobów.

Dalsze przemyślenia

Musimy nie tylko przekazywać metadane stronicowania związane z odpowiedzią, ale także pozwolić klientowi zażądać określonych stron / zakresów. Warto również przyjrzeć się temu aspektowi, aby uzyskać spójne rozwiązanie. Tutaj również możemy użyć nagłówków ( Rangenagłówek wydaje się bardzo odpowiedni) lub innych mechanizmów, takich jak parametry zapytania. Niektórzy ludzie opowiadają traktowanie stron wyników w postaci oddzielnych środków, które mogą mieć sens w niektórych przypadkach zastosowania (np /books/231/pages/52. Skończyło się wybierając dziką gamę najczęściej używanych parametrów żądania takich jak pagesize, page[size]i limitetc oprócz wspierania Rangenagłówek (i jak żądanie parametru także).

Stijn de Witt
źródło
Byłem szczególnie zainteresowany Rangenagłówkiem, ale nie mogłem znaleźć wystarczających dowodów na to, że używanie czegokolwiek innego niż bytestyp zakresu jest prawidłowe.
VisioN
2
Myślę, że najbardziej wyraźne dowody można znaleźć w sekcji 14.5 RFC : acceptable-ranges = 1#range-unit | "none"Myślę, że to sformułowanie wyraźnie pozostawia miejsce na inne jednostki zakresu niż bytes, chociaż sama specyfikacja tylko definiuje bytes.
Stijn de Witt
24

Alternatywa, gdy nie potrzebujesz rzeczywistych przedmiotów

Odpowiedź Franci Penov jest z pewnością najlepszym rozwiązaniem, dlatego zawsze zwracasz elementy wraz ze wszystkimi dodatkowymi metadanymi dotyczącymi żądanych podmiotów. Tak to powinno być zrobione.

ale czasami zwracanie wszystkich danych nie ma sensu, ponieważ możesz ich wcale nie potrzebować. Może wszystko, czego potrzebujesz, to metadane dotyczące żądanego zasobu. Na przykład całkowita liczba lub liczba stron lub coś innego. W takim przypadku zawsze możesz poprosić o zapytanie URL, aby Twoja usługa nie zwracała elementów, a jedynie metadane, takie jak:

/api/members?metaonly=true
/api/members?includeitems=0

lub coś podobnego ...

Robert Koritnik
źródło
10
Osadzenie tych informacji w nagłówkach ma tę zaletę, że możesz wysłać żądanie HEAD, aby uzyskać tylko liczbę.
felixfbecker
1
@felixfbecker dokładnie, dzięki za ponowne wynalezienie koła i zaśmiecanie interfejsów API różnymi rodzajami mechanizmów :)
EralpB
1
@EralpB Dzięki za ponowne wynalezienie koła i zaśmiecanie interfejsów API !? HEAD jest określony w HTTP. metaonlyczy includeitemsnie jest.
felixfbecker
2
@felixfbecker tylko „dokładnie” było przeznaczone dla Ciebie, reszta dotyczy OP. Przepraszam za zamieszanie.
EralpB
REST polega na wykorzystaniu protokołu HTTP i wykorzystywaniu go do tego, do czego był przeznaczony, w jak największym stopniu. W takim przypadku należy użyć Content-Range (RFC7233). Rozwiązania wewnątrz ciała nie są dobre, zwłaszcza, że ​​nie będą działać z HEAD. tworzenie nowych nagłówków zgodnie z sugestią tutaj jest niepotrzebne i błędne.
Vance Shipley
23

Możesz zwrócić licznik jako niestandardowy nagłówek HTTP w odpowiedzi na żądanie HEAD. W ten sposób, jeśli klient chce tylko zliczenia, nie musisz zwracać rzeczywistej listy i nie ma potrzeby stosowania dodatkowego adresu URL.

(Lub, jeśli jesteś w kontrolowanym środowisku od punktu końcowego do punktu końcowego, możesz użyć niestandardowego czasownika HTTP, takiego jak COUNT).

bzlm
źródło
4
„Niestandardowy nagłówek HTTP”? To mogłoby być nieco zaskakujące, co z kolei jest sprzeczne z tym, co moim zdaniem powinno być RESTful API. Ostatecznie nie powinno to być zaskakujące.
Donal Fellows
21
@Donal Wiem. Ale wszystkie dobre odpowiedzi były już zajęte. :(
bzlm
1
Ja też wiem, ale czasami po prostu musisz pozwolić innym ludziom odpowiedzieć. Lub wnieś swój wkład lepiej w inny sposób, na przykład szczegółowe wyjaśnienie, dlaczego należy to zrobić najlepiej, a nie innymi.
Donal Fellows
4
W kontrolowanym środowisku może to nie być zaskakujące, ponieważ prawdopodobnie byłoby używane wewnętrznie i w oparciu o zasady API twoich programistów. Powiedziałbym, że w niektórych przypadkach było to dobre rozwiązanie i warto je tutaj mieć jako notatkę o możliwym nietypowym rozwiązaniu.
James Billingham
1
Bardzo lubię używać nagłówków HTTP do tego typu rzeczy (to naprawdę tam, gdzie to powinno). W tym przypadku odpowiedni może być standardowy nagłówek Link (używa go interfejs API Github).
Mike Marcacci
11

Zalecałbym dodanie nagłówków na to samo, na przykład:

HTTP/1.1 200

Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json

[
  {
    "id": 10,
    "name": "shirt",
    "color": "red",
    "price": "$23"
  },
  {
    "id": 11,
    "name": "shirt",
    "color": "blue",
    "price": "$25"
  }
]

Po szczegóły patrz:

https://github.com/adnan-kamili/rest-api-response-format

Dla pliku swagger:

https://github.com/adnan-kamili/swagger-response-template

adnan kamili
źródło
7

Od „X -” - prefiks został wycofany. (patrz: https://tools.ietf.org/html/rfc6648 )

Okazało się, że „Akceptuj-zakresy” to najlepszy sposób na zmapowanie zakresu paginacji: https://tools.ietf.org/html/rfc7233#section-2.3 Ponieważ „Jednostki zakresu” mogą mieć wartość „bajty” lub „ znak". Obie nie reprezentują niestandardowego typu danych. (patrz: https://tools.ietf.org/html/rfc7233#section-4.2 ) Niemniej jednak stwierdza się, że

Implementacje HTTP / 1.1 MOGĄ ignorować zakresy określone przy użyciu innych jednostek.

Co oznacza: używanie niestandardowych jednostek zakresu nie jest sprzeczne z protokołem, ale MOŻE zostać zignorowane.

W ten sposób musielibyśmy ustawić Accept-Ranges na „członków” lub jakikolwiek typ jednostki dystansowej, jakiego byśmy się spodziewali. Ponadto ustaw zakres zawartości na bieżący zakres. (patrz: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

Tak czy inaczej, trzymałbym się zalecenia RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ), aby wysłać 206 zamiast 200:

Jeśli wszystkie warunki wstępne są spełnione, serwer obsługuje
pole nagłówka Range dla zasobu docelowego, a określone zakresy są
prawidłowe i możliwe do spełnienia (zgodnie z definicją w sekcji 2.1), serwer POWINIEN
wysłać odpowiedź 206 (Treść częściowa) z ładunkiem zawierającym jedną
lub więcej reprezentacji częściowych, które odpowiadają żądanym zadowalającym
zakresom, jak określono w sekcji 4.

W rezultacie otrzymalibyśmy następujące pola nagłówka HTTP:

W przypadku częściowej zawartości:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Pełna treść:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20
Lepidopteron
źródło
3

Wydaje się, że najłatwiej jest po prostu dodać plik

GET
/api/members/count

i zwróć całkowitą liczbę członków

willcodejavaforfood
źródło
11
Nie jest to dobry pomysł. Zobowiązujesz klientów do złożenia 2 próśb o zbudowanie paginacji na swoich stronach. Pierwsza prośba o uzyskanie listy zasobów, a druga o policzenie całości.
Jekis,
Myślę, że to dobre podejście ... możesz również zwrócić tylko listę wyników jako json i po stronie klienta sprawdzić rozmiar kolekcji, więc taki przypadek jest głupi przykład ... ponadto możesz mieć / api / members / count, a następnie / api / Members? offset = 10 & limit = 20
Michał Ziobro
1
Należy również pamiętać, że wiele rodzajów paginacji nie wymaga liczenia (np. Nieskończone przewijanie) - Po co to obliczać, gdy klient może tego nie potrzebować
tofarr
2

A co z nowym punktem końcowym> / api / members / count, który po prostu wywołuje Members.Count () i zwraca wynik

Steve Woods
źródło
27
Nadanie licznikowi jawnego punktu końcowego sprawia, że ​​jest to samodzielny zasób adresowalny. To zadziała, ale wywoła interesujące pytania dla każdego nowego interfejsu API - czy liczba członków kolekcji to osobny zasób z kolekcji? Czy mogę zaktualizować go żądaniem PUT? Czy istnieje dla pustej kolekcji, czy tylko wtedy, gdy są w niej elementy? Jeśli memberskolekcja może zostać utworzona przez żądanie POST do /api, zostanie /api/members/countrównież utworzona jako efekt uboczny, czy też muszę wykonać jawne żądanie POST, aby ją utworzyć przed zażądaniem? :-)
Franci Penov
2

Czasami frameworki (takie jak $ resource / AngularJS) wymagają tablicy jako wyniku zapytania i tak naprawdę nie możesz mieć odpowiedzi, tak jak {count:10,items:[...]}w tym przypadku przechowuję „count” w responseHeaders.

PS Właściwie możesz to zrobić za pomocą $ resource / AngularJS, ale wymaga to pewnych poprawek.

Vahe Hovhannisyan
źródło
Co to za poprawki?
Byliby
Angular nie WYMAGA tablicy jako wyniku zapytania, wystarczy skonfigurować zasób z właściwością obiektu opcji:isArray: false|true
Rémi Becheras
0

Możesz rozważyć countsjako zasób. Adres URL miałby wtedy postać:

/api/counts/member
Frank Rem
źródło
-1

Żądając danych podzielonych na strony, znasz (poprzez jawną wartość parametru rozmiaru strony lub domyślną wartość rozmiaru strony) rozmiar strony, więc wiesz, czy otrzymałeś wszystkie dane w odpowiedzi, czy nie. Kiedy w odpowiedzi jest mniej danych niż rozmiar strony, otrzymujesz pełne dane. Po zwróceniu pełnej strony musisz ponownie poprosić o kolejną stronę.

Wolę mieć oddzielny punkt końcowy dla count (lub ten sam punkt końcowy z parametrem countOnly). Ponieważ możesz przygotować użytkownika końcowego na długi / czasochłonny proces, wyświetlając odpowiednio zainicjowany pasek postępu.

Jeśli chcesz zwrócić rozmiar danych w każdej odpowiedzi, powinieneś również podać pageSize i przesunięcie. Szczerze mówiąc, najlepszym sposobem jest również powtórzenie filtrów żądań. Ale odpowiedź stała się bardzo złożona. Dlatego wolę dedykowany punkt końcowy, aby zwracać liczbę.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Moje połączenie, wolę parametr countOnly do istniejącego punktu końcowego. Tak więc, jeśli zostanie określony, odpowiedź zawiera tylko metadane.

punkt końcowy? filtr = wartość

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

endpoint? filter = value & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Wooff
źródło