Paginacja w aplikacji internetowej REST

329

Jest to bardziej ogólne przeformułowanie tego pytania (z wyeliminowaniem określonych części Railsów)

Nie jestem pewien, jak zaimplementować paginację na zasobie w aplikacji sieci Web RESTful. Zakładając, że mam zasób o nazwie products, który z poniższych według Ciebie jest najlepszym podejściem i dlaczego:

1. Używanie tylko ciągów zapytań

na przykład. http://application/products?page=2&sort_by=date&sort_how=asc
Problem polega na tym, że nie mogę używać pełnego buforowania stron, a także adres URL nie jest bardzo czysty i łatwy do zapamiętania.

2. Używanie stron jako zasobów i ciągów zapytań do sortowania

na przykład. http://application/products/page/2?sort_by=date&sort_how=asc
W takim przypadku widzi się problem polegający na tym, że http://application/products/pages/1nie jest to unikalny zasób, ponieważ użycie sort_by=pricemoże dać zupełnie inny wynik i nadal nie mogę używać buforowania stron.

3. Używanie stron jako zasobów i segmentu adresów URL do sortowania

na przykład. http://application/products/by-date/page/2
Osobiście nie widzę problemu z użyciem tej metody, ale ktoś ostrzegł mnie, że nie jest to dobra droga (nie podał powodu, więc jeśli wiesz, dlaczego nie jest to zalecane, daj mi znać)

Wszelkie sugestie, opinie, krytyki są mile widziane. Dzięki.

i ja
źródło
34
To świetne pytanie.
Iain Holder
7
Pytanie dodatkowe: w jaki sposób ludzie zazwyczaj określają rozmiary stron?
Heiko Rupp
Nie zapomnij o parametrach Matrix w3.org/DesignIssues/MatrixURIs.html
CMCDragonkai

Odpowiedzi:

66

Myślę, że problem z wersją 3 jest bardziej problemem z punktu widzenia - czy widzisz stronę jako zasób czy produkty na stronie.

Jeśli widzisz stronę jako zasób, jest to doskonałe rozwiązanie, ponieważ zapytanie o stronę 2 zawsze da stronę 2.

Ale jeśli widzisz produkty na stronie jako zasób, masz problem z tym, że produkty na stronie 2 mogą się zmienić (stare produkty usunięte lub cokolwiek innego), w tym przypadku identyfikator URI nie zawsze zwraca te same zasoby.

Np. Klient przechowuje link do strony X listy produktów, przy następnym otwarciu tego łącza dany produkt może już nie znajdować się na stronie X.

Fionn
źródło
6
Cóż, ale jeśli coś usuniesz, na tym samym URI nie powinno być czegoś innego. Jeśli usuniesz wszystkie produkty ze strony X - strona X może nadal być ważna, ale teraz zawiera produkty ze strony X + 1. Tak więc identyfikator URI strony X stał się identyfikatorem URI strony X + 1, jeśli widzisz go w „widoku zasobów produktu” „.
Fionn
1
> Jeśli widzisz stronę jako zasób, jest to doskonale rozwiązanie, ponieważ zapytanie o stronę 2 zawsze da stronę 2. Czy to ma w ogóle sens? Ten sam adres URL (dowolny adres URL wymieniający stronę 2) zawsze da stronę 2, bez względu na to, co Ty jako zasób.
temoto
2
Widzenie strony jako zasobu prawdopodobnie powinno wprowadzić POST / foo / page, aby utworzyć nową stronę, prawda?
temoto
18
Twoja odpowiedź płynnie brzmi „poprawne rozwiązanie to 1”, ale nie podaje tego.
temoto
2
Moim zdaniem strona jest pojęciem swobodnym i nie jest związana z podstawową domeną. I dlatego nie należy go uważać za zasób. Mam na myśli pływanie w tym sensie, że jest płynne, że pojęcie strony zmienia się wraz z kontekstem; jeden użytkownik twojego API może być aplikacją mobilną, która może zużywać tylko 2 produkty na stronę, podczas gdy drugi jest aplikacją maszynową, która może zużywać całą cholerną listę. Krótko mówiąc, strona jest „reprezentacją” podstawowej jednostki domeny (produktu) i nie powinna być uwzględniana jako część adresu URL; tylko jako parametr zapytania.
Kingz
106

Zgadzam się z Fionn, ale pójdę o krok dalej i powiem, że strona nie jest zasobem, jest własnością żądania. To sprawia, że ​​wybrałem tylko ciąg zapytania opcji 1. Po prostu czuje się dobrze. Naprawdę podoba mi się struktura interfejsu API Twittera . Niezbyt proste, niezbyt skomplikowane, dobrze udokumentowane. Na dobre lub na złe to mój projekt „idź do”, kiedy jestem na płocie i robię coś w jedną stronę w drugą stronę.

slf
źródło
28
+1: ciągi zapytań nie są pierwszorzędnymi identyfikatorami zasobów; po prostu wyjaśniają sposób zamawiania i grupowania zasobów.
S.Lott,
1
@ S.Lott Żądanie jest zasobem. To, co nazywacie „pierwszorzędnymi zasobami”, jest definiowane jako wartości przez Fielding w sekcji 5.2.1.1 jego rozprawy . Ponadto w tej samej sekcji Fielding podaje najnowszą wersję pliku kodu źródłowego jako przykład zasobu. Jak może to być zasób, ale ostatnie 10 produktów może być „właściwościami żądania dotyczącego zasobu produktów”? Rozumiem, że twój pogląd jest bardziej praktyczny, ale myślę, że jest mniej RESZTNY.
edsioufi,
Zauważ, że mój komentarz nie oznacza, że ​​nie zgadzam się z wyborem użycia ciągów zapytań nad adresami URL: oba są realnymi rozwiązaniami, o ile interfejs API jest oparty na hipermediach, jak wspomniał @RichApodaca w swojej odpowiedzi. Właśnie wskazuję, że stronę należy traktować jako zasób z punktu widzenia REST.
edsioufi,
37

HTTP ma świetny nagłówek Range, który nadaje się również do stronicowania. Możesz wysłać

Range: pages=1

mieć tylko pierwszą stronę. Może to zmusić Cię do ponownego przemyślenia, co to jest strona. Może klient chce innej gamy produktów. Nagłówek zakresu działa również w celu zadeklarowania zamówienia:

Range: products-by-date=2009_03_27-

aby wszystkie produkty były nowsze niż ta data lub

Range: products-by-date=0-2009_11_30

aby wszystkie produkty były starsze niż ta data. „0” prawdopodobnie nie jest najlepszym rozwiązaniem, ale RFC wydaje się chcieć czegoś na początek zakresu. Mogą być wdrożone parsery HTTP, które nie analizowałyby jednostek = -range_end.

Jeśli nagłówki nie są (akceptowalną) opcją, uważam, że pierwsze rozwiązanie (wszystko w ciągu zapytania) jest sposobem na radzenie sobie ze stronami. Ale proszę znormalizować ciągi zapytań (pary sortowania (klucz = wartość) w kolejności alfabetycznej). Rozwiązuje to problem różnicowania „? A = 1 & b = x” i „? B = x & a = 1”.

temoto
źródło
34
nagłówki mogą na pierwszy rzut oka wyglądać ładnie, ale nie pozwalają na udostępnianie strony (np. przez skopiowanie adresu URL). Dla żądania ajax mogą być dobrym rozwiązaniem (ponieważ strony zmodyfikowane przez ajax nie mogą być udostępniane w ich bieżącym stanie), ale nie użyłbym ich do regularnego paginacji.
Markus,
3
A nagłówek Range dotyczy tylko zakresów bajtów. Zobacz [specyfikacja nagłówków HTTP] ( w3.org/Protocols/rfc2616/rfc2616-sec14.html ), sekcja 14.35.
Chris Westin
16
@ChrisWestin w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 HTTP / 1.1 używa jednostek zakresu w polach nagłówka Range (sekcja 14.35) i Content-Range (sekcja 14.16). range-unit = bytes-unit | other-range-unit Może masz na myśli The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1 implementations MAY ignore ranges specified using other units.To nie jest to samo, co twoje oświadczenie.
temoto
1
@Markus Nie mogę sobie wyobrazić przypadku użycia, gdy udostępniasz zasób interfejsu API odpoczynku :)
JakubKnejzlik,
@JakubKnejzlik Udostępnianie nie stanowi problemu, ale użycie nagłówków HTTP do stronicowania uniemożliwia korzystanie z łączy HATEOAS do stronicowania.
xarx
25

Opcja 1 wydaje się najlepsza, o ile aplikacja postrzega paginację jako technikę tworzenia odmiennego widoku tego samego zasobu.

Powiedziawszy to, schemat adresów URL jest stosunkowo nieznaczny. Jeśli projektujesz aplikację tak, aby działała w oparciu o hipertekst (ponieważ wszystkie aplikacje REST muszą być z definicji), wtedy twój klient nie będzie sam tworzył żadnych identyfikatorów URI. Zamiast tego Twoja aplikacja będzie podawać linki do klienta, a klient będzie ich śledził.

Jednym z rodzajów linków, które może dostarczyć klient, jest link do stronicowania.

Przyjemnym efektem ubocznym tego wszystkiego jest to, że nawet jeśli zmienisz zdanie na temat struktury URI paginacji i zaimplementujesz coś zupełnie innego w przyszłym tygodniu, Twoi klienci mogą kontynuować pracę bez żadnych modyfikacji.

Rich Apodaca
źródło
3
Ładne przypomnienie o korzystaniu z hipermediów takich jak łącza w usługach internetowych REST.
Paul D. Eden,
11

Zawsze korzystałem ze stylu opcji 1. Buforowanie nie stanowiło problemu, ponieważ dane i tak często się zmieniają w moim przypadku. Jeśli zezwolisz na konfigurację rozmiaru strony, ponownie nie będzie można buforować danych.

Nie uważam adresu URL za trudny do zapamiętania lub nieczysty. Dla mnie jest to dobre wykorzystanie parametrów zapytania. Zasób jest wyraźnie listą produktów, a parametry zapytania mówią tylko, jak chcesz wyświetlić listę - posortowaną i na której stronie.

John Snyders
źródło
1
+1 Chyba masz rację, a ja pójdę z parametrami zapytania (opcja 1)
Andi
„Nie uważam adresu URL za trudny do zapamiętania”. Ta obserwacja jest bezużyteczna w aplikacjach REST, ponieważ zazwyczaj powinny one mieć tylko jedną zakładkę ... Jeśli użytkownik (lub aplikacja kliencka) próbuje „zapamiętać” adres URL, jest to dobry znak, że interfejs API nie jest spokojny.
edsioufi,
8

Dziwne, że nikt nie zauważył, że Opcja 3 ma parametry w określonej kolejności. http // aplikacja / produkty / data / malejąco / nazwa / rosnąco / strona / 2 i http // aplikacja / produkty / nazwa / rosnąco / data / malejąco / strona / 2

wskazują na ten sam zasób, ale mają zupełnie inne adresy URL.

Dla mnie opcja 1 wydaje się najbardziej akceptowalna, ponieważ wyraźnie oddziela „Co chcę” i „Jak chcę” (ma nawet znak zapytania między nimi lol). Buforowanie całej strony można wdrożyć przy użyciu pełnego adresu URL (wszystkie opcje i tak będą miały ten sam problem).

Przy podejściu Parameters-in-URL jedyną korzyścią jest czysty adres URL. Chociaż musisz wymyślić jakiś sposób na kodowanie parametrów i bezstratne ich dekodowanie. Oczywiście możesz przejść z URLencode / decode, ale to znowu sprawi, że adresy URL będą brzydkie :)

TEHEK
źródło
1
Są to dwa różne porządki. Pierwszy sortuje według daty malejącej i zrywa więzi według nazwy rosnąco; drugi sortuje według nazwy rosnąco i zrywa więzi według daty malejącej.
Imran Rashid
W rzeczywistości dwa podane tutaj przykładowe adresy URL różnią się nie tylko pisaniem, ale także znaczeniem. Ponieważ oznacza ścieżkę, nie ma żadnej gwarancji, że znajdziesz to samo, skręcając najpierw w lewo i w prawo potem i odwrotnie. Powiedziawszy to, posortuj parametry jako części ścieżki URL, które mają formalną przewagę nad parametrami URL, które powinny być wymienialne wymiennie bez zmiany ogólnego znaczenia, ale faktycznie cierpią z powodu kodowania pułapek, jak tu powiedziano.
Christian Gosch,
7

Wolę używać przesunięcia i ograniczenia parametrów zapytania.

offset : dla indeksu pozycji w kolekcji.

limit : do zliczenia przedmiotów.

Klient może po prostu aktualizować przesunięcie w następujący sposób

offset = offset + limit

na następną stronę.

Ścieżka jest uważana za identyfikator zasobu. A strona nie jest zasobem, ale podzbiorem kolekcji zasobów. Ponieważ paginacja jest ogólnie żądaniem GET, parametry zapytania najlepiej nadają się do paginacji, a nie nagłówków.

Odniesienie: https://metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page

Sorter
źródło
5

W poszukiwaniu najlepszych praktyk natknąłem się na tę stronę:

http://www.restapitutorial.com

Na stronie zasobów znajduje się link do pobrania pliku .pdf, który zawiera kompletne najlepsze praktyki REST sugerowane przez autora. W którym między innymi znajduje się sekcja na temat paginacji.

Autor sugeruje dodanie obsługi zarówno przy użyciu nagłówka Range, jak i parametrów ciągu zapytania.

Żądanie

Przykład nagłówka HTTP:

Range: items=0-24

Przykład parametrów ciągu zapytania:

GET http://api.example.com/resources?offset=0&limit=25

Gdzie przesunięcie jest początkowym numerem przedmiotu, a limit to maksymalna liczba przedmiotów do zwrotu.

Odpowiedź

Odpowiedź powinna zawierać nagłówek Content-Range wskazujący, ile elementów jest zwracanych i ile elementów istnieje jeszcze do odzyskania

Przykłady nagłówków HTTP:

Content-Range: items 0-24/66

Content-Range: items 40-65/*

W pliku pdf znajdują się inne sugestie dotyczące bardziej szczegółowych przypadków.

Mario Arturo
źródło
4

Obecnie używam schematu podobnego do tego w moich aplikacjach ASP.NET MVC:

na przykład http://application/products/by-date/page/2

konkretnie jest to: http://application/products/Date/Ascending/3

Jednak nie jestem zadowolony z włączenia w ten sposób informacji stronicowania i sortowania do trasy.

Lista przedmiotów (w tym przypadku produktów) jest zmienna. tj. następnym razem, gdy ktoś powróci do adresu URL zawierającego parametry stronicowania i sortowania, wyniki, które otrzymają, mogły ulec zmianie. Tak więc pomysł http://application/products/Date/Ascending/3na unikalny adres URL wskazujący określony, niezmienny zestaw produktów został utracony.

Steve Willcock
źródło
1
Pierwszy problem, z sortowaniem według wielu kolumn, moim zdaniem dotyczy wszystkich 3 metod. Więc to nie jest tak naprawdę żadna z nich. Odnośnie drugiego problemu: czy to nie może się zdarzyć z żadnym zasobem? Na przykład produkt można również edytować / usuwać.
andi
Myślę, że sortowanie według wielu kolumn jest naprawdę „oszustwem” dla wszystkich 3 metod, ponieważ URL staje się większy i trudniejszy do zarządzania - stąd jeden z powodów, dla których rozważam przejście na parametry strony / sortowania oparte na formularzach. W przypadku drugiego wydania uważam, że istnieje zasadnicza różnica koncepcyjna między unikalnym trwałym identyfikatorem, takim jak identyfikator produktu, a przejściową listą produktów. W przypadku usuniętych produktów komunikat np. „Ten produkt nie istnieje w systemie” mówi coś konkretnego o tym produkcie.
Steve Willcock
1
Dobrze jest usunąć wszystkie informacje stronicowania i sortowania z trasy. I pchanie go do parametrów POST jest złe. Witaj? Pytanie dotyczy REST. Nie używamy POST tylko po to, aby skrócić adres URL w REST. Czasownik ma sens.
temoto
1
Osobiście nie użyłbym parametrów formularza do zapytania, ponieważ prawie wymagałoby to metody POST lub PUT HTTP (ponieważ w żądaniu jest teraz treść). Wydaje mi się, że GET jest bardziej odpowiednią metodą, ponieważ zarówno POST, jak i PUT sugerują modyfikację zasobu. Z tego powodu wybrałbym więcej parametrów zapytania do adresu URL, gdy potrzebne jest sortowanie według wielu kolumn.
Paul D. Eden,
1

Zgadzam się ze slf, że „strona” nie jest tak naprawdę zasobem. Z drugiej strony opcja 3 jest czystsza, łatwiejsza do odczytania, a użytkownik może ją łatwiej odgadnąć, a nawet wpisać w razie potrzeby. Jestem rozdarty pomiędzy opcjami 1 i 3, ale nie widzę żadnego powodu, aby nie używać opcji 3.

Ponadto, chociaż wyglądają ładnie, jednym z minusów używania ukrytych parametrów, jak ktoś wspomniał, zamiast ciągów zapytań lub segmentów adresów URL jest to, że użytkownik nie może utworzyć zakładki ani bezpośrednio przejść do konkretnej strony. W zależności od aplikacji może to być problem, ale może to być coś, o czym należy pamiętać.

insane.dreamer
źródło
1
Jeśli chodzi o twoją wzmiankę o łatwiejszym zgadywaniu, nie powinno to mieć znaczenia. W przypadku tworzenia interfejsu API hypermedia użytkownicy nigdy nie powinni zgadywać identyfikatorów URI.
JR Garcia,
0

Używałem wcześniej rozwiązania 3 (piszę DUŻO aplikacji django). I nie sądzę, żeby było w tym coś złego. Jest tak samo generowalny jak pozostałe dwa (w tym przypadku musisz wykonać masowe skrobanie itp.) I wygląda na czystszy. Ponadto użytkownicy mogą odgadnąć adresy URL (jeśli jest to aplikacja publiczna), a ludzie lubią być w stanie iść bezpośrednio tam, gdzie chcą, a odgadywanie adresów URL jest silniejsze.

Alex
źródło
0

Używam w moich projektach następujących adresów URL:

http://application/products?page=2&sort=+field1-field2

co oznacza - „podaj mi drugą stronę uporządkowaną rosnąco według pola 1, a następnie malejąco według pola 2”. Lub jeśli potrzebuję jeszcze większej elastyczności, używam:

http://application/products?skip=20&limit=20&sort=+field1-field2
Eugene
źródło
0

Używam następujących wzorów, aby uzyskać rekord następnej strony. http: // application / products? lastRecordKey =? & pageSize = 20 & sort = ASC

RecordKey to kolumna tabeli, która zawiera wartość sekwencyjną w DB. Służy do pobierania tylko jednej strony danych z DB. pageSize służy do określenia liczby rekordów do pobrania. Sortuj służy do sortowania rekordu w kolejności rosnącej lub malejącej.

Susanta Ghosh
źródło