Pagination dalam aplikasi web REST

329

Ini adalah reformulasi yang lebih umum dari pertanyaan ini (dengan menghilangkan bagian-bagian spesifik Rails)

Saya tidak yakin bagaimana menerapkan pagination pada sumber daya dalam aplikasi web yang tenang. Dengan asumsi bahwa saya memiliki sumber daya yang disebut products, menurut Anda yang manakah di bawah ini yang merupakan pendekatan terbaik, dan mengapa:

1. Hanya menggunakan string kueri

misalnya. http://application/products?page=2&sort_by=date&sort_how=asc
Masalahnya di sini adalah bahwa saya tidak dapat menggunakan caching halaman penuh dan juga URL tidak sangat bersih dan mudah diingat.

2. Menggunakan halaman sebagai sumber daya dan string kueri untuk menyortir

misalnya. http://application/products/page/2?sort_by=date&sort_how=asc
Dalam hal ini, masalah yang dilihat adalah bahwa http://application/products/pages/1itu bukan sumber daya yang unik karena menggunakan sort_by=pricedapat menghasilkan hasil yang sama sekali berbeda dan saya masih tidak dapat menggunakan caching halaman.

3. Menggunakan halaman sebagai sumber daya dan segmen URL untuk menyortir

misalnya. http://application/products/by-date/page/2
Saya pribadi tidak melihat masalah dalam menggunakan metode ini, tetapi seseorang memperingatkan saya bahwa ini bukan cara yang baik untuk pergi (dia tidak memberikan alasan, jadi jika Anda tahu mengapa itu tidak disarankan, beri tahu saya)

Setiap saran, opini, kritik lebih dari diterima. Terima kasih.

dan saya
sumber
34
Ini pertanyaan yang bagus.
Iain Holder
7
Pertanyaan bonus: bagaimana orang biasanya menentukan ukuran halaman?
Heiko Rupp
Jangan lupa tentang parameter Matriks w3.org/DesignIssues/MatrixURIs.html
CMCDragonkai

Jawaban:

66

Saya pikir masalah dengan versi 3 lebih merupakan masalah "sudut pandang" - apakah Anda melihat halaman sebagai sumber daya atau produk di halaman.

Jika Anda melihat halaman sebagai sumber daya itu adalah solusi yang sangat baik, karena permintaan untuk halaman 2 akan selalu menghasilkan halaman 2.

Tetapi jika Anda melihat produk pada halaman sebagai sumber daya Anda memiliki masalah bahwa produk pada halaman 2 dapat berubah (produk lama dihapus, atau apa pun), dalam hal ini URI tidak selalu mengembalikan sumber daya yang sama.

Misalnya, Pelanggan menyimpan tautan ke daftar produk halaman X, saat berikutnya tautan dibuka produk yang dimaksud mungkin tidak lagi ada di halaman X.

Fionn
sumber
6
Baiklah, tetapi jika Anda menghapus sesuatu, seharusnya tidak ada sesuatu yang lain pada URI yang sama. Jika Anda menghapus semua produk dari halaman X - halaman X mungkin masih valid tetapi sekarang berisi produk dari halaman X + 1. Jadi URI untuk halaman X telah menjadi URI untuk halaman X + 1 jika Anda melihatnya di "tampilan sumber daya produk" ".
Fionn
1
> Jika Anda melihat halaman sebagai sumber daya itu adalah solusi yang sangat baik, karena permintaan untuk halaman 2 akan selalu menghasilkan halaman 2. Apakah itu masuk akal? URL yang sama (URL apa pun yang menyebutkan halaman 2) akan selalu menghasilkan halaman 2 apa pun sumber daya Anda.
temoto
2
Melihat halaman sebagai sumber mungkin harus memperkenalkan POST / foo / halaman untuk membuat halaman baru, kan?
temoto
18
Jawaban Anda dengan lancar mengarah ke "solusi yang benar adalah 1", tetapi tidak menyatakannya.
temoto
2
Dalam pikiran saya, halaman adalah konsep mengambang, dan tidak terkait dengan domain yang mendasarinya. Dan karena itu tidak boleh dianggap sebagai sumber daya. Maksud saya mengambang dalam arti bahwa itu cairan, bahwa konsep halaman berubah dengan konteksnya; satu pengguna API Anda mungkin merupakan aplikasi seluler, yang hanya dapat mengonsumsi 2 produk per halaman, sementara yang lain adalah aplikasi mesin yang dapat mengkonsumsi seluruh daftar sialan. Singkatnya, halaman adalah "representasi" dari entitas domain yang mendasari (produk) dan tidak boleh dimasukkan sebagai bagian dari URL; hanya sebagai parameter kueri.
Kingz
106

Saya setuju dengan Fionn, tetapi saya akan melangkah lebih jauh dan mengatakan bahwa bagi saya Page bukanlah sumber daya, itu adalah properti dari permintaan. Itu membuat saya memilih opsi 1 string kueri saja. Rasanya benar. Saya sangat suka bagaimana Twitter API terstruktur dengan tenang. Tidak terlalu sederhana, tidak terlalu rumit, didokumentasikan dengan baik. Lebih baik atau lebih buruk itu desain "pergi ke" saya ketika saya berada di pagar melakukan sesuatu satu arah versus yang lain.

slf
sumber
28
+1: string kueri bukan pengidentifikasi sumber daya kelas satu; mereka hanya klarifikasi untuk pemesanan dan pengelompokan sumber daya.
S.Lott
1
@ S.Lott Permintaan adalah sumber dayanya. Apa yang Anda sebut "sumber daya kelas" didefinisikan sebagai nilai - nilai oleh Fielding di bagian 5.2.1.1 disertasinya . Selanjutnya, di bagian yang sama, Fielding memberikan Revisi Terbaru dari file kode sumber sebagai contoh sumber daya. Bagaimana itu bisa menjadi sumber daya tetapi 10 produk terbaru menjadi "properti permintaan pada sumber daya produk"? Saya mengerti bahwa pandangan Anda lebih praktis, tetapi saya pikir itu kurang tenang.
edsioufi
Perhatikan bahwa komentar saya tidak berarti bahwa saya tidak setuju dengan pilihan menggunakan string kueri di atas URL: keduanya merupakan solusi yang layak selama API digerakkan oleh hypermedia, seperti yang disebutkan oleh @RichApodaca dalam jawabannya. Saya hanya menunjukkan bahwa Halaman harus dianggap sebagai sumber daya dari sudut pandang REST.
edsioufi
37

HTTP memiliki Range header yang bagus yang juga cocok untuk pagination. Anda dapat mengirim

Range: pages=1

hanya memiliki halaman pertama. Itu mungkin memaksa Anda untuk memikirkan kembali apa itu halaman. Mungkin klien menginginkan rentang item yang berbeda. Header rentang juga berfungsi untuk mendeklarasikan pesanan:

Range: products-by-date=2009_03_27-

untuk mendapatkan semua produk yang lebih baru dari tanggal itu atau

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

untuk mendapatkan semua produk yang lebih tua dari tanggal itu. '0' mungkin bukan solusi terbaik, tetapi RFC tampaknya menginginkan sesuatu untuk rentang awal. Mungkin ada parser HTTP yang digunakan yang tidak akan menguraikan unit = -range_end.

Jika tajuk bukan pilihan (dapat diterima), saya rasa solusi pertama (semua dalam string kueri) adalah cara untuk menangani halaman. Tapi tolong, normalisasikan string kueri (sort (key = value) berpasangan dalam urutan alfabet). Ini memecahkan masalah diferensiasi "? A = 1 & b = x" dan "? B = x & a = 1".

temoto
sumber
34
header mungkin terlihat bagus pada pandangan pertama, tetapi mereka melarang berbagi halaman (misalnya dengan menyalin url). Jadi untuk permintaan ajax mereka mungkin merupakan solusi yang bagus (karena halaman yang dimodifikasi oleh ajax tidak dapat dibagikan dalam keadaan mereka saat ini), tapi saya tidak akan menggunakannya untuk pagination biasa.
Markus
3
Dan header Range hanya untuk rentang byte. Lihat [spec HTTP header] ( w3.org/Protocols/rfc2616/rfc2616-sec14.html ), bagian 14.35.
Chris Westin
16
@ChrisWestin w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 HTTP / 1.1 menggunakan unit rentang dalam bidang header Range (bagian 14.35) dan Content-Range (bagian 14.16). range-unit = bytes-unit | other-range-unit Mungkin yang Anda maksudkan The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1 implementations MAY ignore ranges specified using other units.itu tidak sama dengan pernyataan Anda.
temoto
1
@ Markus Saya tidak bisa membayangkan kasus penggunaan ketika Anda berbagi sumber daya api sisanya :)
JakubKnejzlik
Berbagi @JakubKnejzlik bukan masalah, tetapi menggunakan header HTTP untuk paging mencegah penggunaan tautan HATEOAS untuk paging.
xarx
25

Opsi 1 tampaknya yang terbaik, sejauh aplikasi Anda memandang pagination sebagai teknik untuk menghasilkan tampilan yang berbeda dari sumber daya yang sama.

Karena itu, skema URL relatif tidak signifikan. Jika Anda mendesain aplikasi Anda untuk menjadi hypertext-driven (karena semua aplikasi REST harus berdasarkan definisi), maka klien Anda tidak akan membuat URI sendiri. Sebaliknya, aplikasi Anda akan memberikan tautan ke klien dan klien akan mengikuti mereka.

Salah satu jenis tautan yang dapat disediakan oleh klien Anda adalah tautan pagination.

Efek samping yang menyenangkan dari semua ini adalah bahwa bahkan jika Anda berubah pikiran tentang struktur URI pagination dan menerapkan sesuatu yang sama sekali berbeda minggu depan, klien Anda dapat terus bekerja tanpa modifikasi apa pun.

Apodaca kaya
sumber
3
Pengingat yang bagus tentang penggunaan hypermedia like links di layanan web REST.
Paul D. Eden
11

Saya selalu menggunakan gaya opsi 1. Caching tidak menjadi masalah karena data sering berubah dalam kasus saya. Jika Anda membiarkan ukuran halaman dapat dikonfigurasi maka sekali lagi data tidak bisa di-cache.

Saya tidak menemukan url sulit untuk diingat atau tidak bersih. Bagi saya ini adalah penggunaan parameter query yang bagus. Sumber daya jelas daftar produk dan params pertanyaan hanya memberitahu bagaimana Anda ingin daftar ditampilkan - diurutkan dan halaman mana.

John Snyders
sumber
1
+1 Saya pikir Anda benar dan saya akan pergi dengan parameter kueri (opsi 1)
andi
"Saya tidak menemukan URL yang sulit untuk diingat". Pengamatan ini tidak berguna dalam aplikasi REST, karena yang biasanya hanya memiliki satu bookmark tunggal ... Jika pengguna (atau aplikasi klien) mencoba "mengingat" URL, ini adalah pertanda baik bahwa API tidak tenang.
edsioufi
8

Aneh bahwa tidak ada yang menunjukkan bahwa Opsi 3 memiliki parameter dalam urutan tertentu. http // aplikasi / produk / Tanggal / Turun / Nama / Naik / halaman / 2 dan http // aplikasi / produk / Nama / Naik / Tanggal / Turun / halaman / 2

menunjuk ke sumber yang sama, tetapi memiliki url yang sama sekali berbeda.

Bagi saya Opsi 1 tampaknya yang paling dapat diterima, karena ia dengan jelas memisahkan "Apa yang saya inginkan" dan "Bagaimana saya ingin" itu (Itu bahkan memiliki tanda tanya di antara mereka lol). Caching satu halaman penuh dapat diimplementasikan menggunakan URL lengkap (toh semua opsi akan mengalami masalah yang sama).

Dengan pendekatan Parameter-in-URL, satu-satunya manfaat adalah URL bersih. Meskipun Anda harus datang dengan beberapa cara untuk menyandikan parameter dan mendekode lossless mereka. Tentu saja Anda bisa menggunakan URLencode / decode, tetapi itu akan membuat url jelek lagi :)

TEHEK
sumber
1
Itu adalah dua urutan berbeda. Jenis pertama berdasarkan tanggal turun, dan hanya memutuskan hubungan dengan nama naik; jenis kedua menurut nama naik, dan hanya memutuskan ikatan berdasarkan tanggal turun.
Imran Rashid
Sebenarnya dua contoh URL yang diberikan di sini tidak hanya berbeda dengan menulis, tetapi juga dengan makna. Karena menunjukkan jalan, tidak ada jaminan yang diberikan bahwa Anda menemukan hal yang sama ketika belok kiri pertama dan kanan setelah itu atau sebaliknya. Karena itu, sortir parameter sebagai bagian jalur URL memiliki keunggulan formal atas parameter URL yang harus dapat ditukar secara komutatif tanpa mengubah arti keseluruhan, tetapi memang menderita karena pengodean perangkap seperti yang dikatakan di sini.
Christian Gosch
7

Saya lebih suka menggunakan parameter kueri offset dan batas.

offset : untuk indeks item dalam koleksi.

limit : untuk jumlah item.

Klien dapat terus memperbarui offset sebagai berikut

offset = offset + limit

untuk halaman selanjutnya.

Path dianggap sebagai pengidentifikasi sumber daya. Dan sebuah halaman bukanlah sumber tetapi subset dari koleksi sumber daya. Karena pagination pada umumnya adalah permintaan GET, parameter kueri paling cocok untuk pagination daripada header.

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

Tukang sortir
sumber
5

Mencari praktik terbaik saya menemukan situs ini:

http://www.restapitutorial.com

Di halaman sumber daya ada tautan untuk mengunduh .pdf yang berisi praktik terbaik lengkap REST yang disarankan oleh penulis. Di mana antara lain ada bagian tentang pagination.

Penulis menyarankan untuk menambahkan dukungan untuk keduanya menggunakan header Range dan menggunakan parameter string-string.

Permintaan

Contoh tajuk HTTP:

Range: items=0-24

Contoh parameter string-string:

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

Di mana offset adalah nomor item awal dan batas adalah jumlah item maksimum yang akan dikembalikan.

Tanggapan

Respons harus mencakup tajuk Kisaran Konten yang menunjukkan berapa banyak item yang dikembalikan dan berapa jumlah total item yang belum diambil

Contoh tajuk HTTP:

Content-Range: items 0-24/66

Content-Range: items 40-65/*

Dalam .pdf ada beberapa saran lain untuk kasus yang lebih spesifik.

Mario Arturo
sumber
4

Saat ini saya menggunakan skema yang mirip dengan ini di aplikasi ASP.NET MVC saya:

misalnya http://application/products/by-date/page/2

khususnya itu: http://application/products/Date/Ascending/3

Namun, saya tidak benar-benar senang dengan memasukkan halaman dan menyortir informasi dalam rute dengan cara ini.

Daftar item (produk dalam hal ini) bisa berubah. yaitu saat berikutnya seseorang kembali ke url yang mencakup parameter paging dan sortir, hasil yang didapat mungkin telah berubah. Jadi gagasan http://application/products/Date/Ascending/3sebagai url unik yang menunjuk ke rangkaian produk yang ditentukan dan tidak berubah hilang.

Steve Willcock
sumber
1
Masalah pertama, dengan mengurutkan pada beberapa kolom, berlaku untuk semua 3 metode menurut saya. Jadi itu tidak benar-benar pro / kontra untuk mereka. Mengenai masalah kedua: tidak bisakah itu terjadi pada sumber daya apa pun ? Suatu produk, misalnya, juga dapat diedit / dihapus.
andi
Saya pikir menyortir pada banyak kolom adalah benar-benar 'con' untuk semua 3 metode karena url semakin besar dan lebih tidak terkelola - karena itu salah satu alasan saya mempertimbangkan untuk pindah ke bentuk halaman berdasarkan / mengurutkan parameter. Untuk masalah kedua, saya pikir ada perbedaan konseptual mendasar antara pengenal persisten unik seperti id produk daripada daftar produk sementara. Untuk produk yang dihapus pesan misalnya 'Produk itu tidak ada di sistem' memberi tahu Anda sesuatu yang konkret tentang produk itu.
Steve Willcock
1
Menghapus semua halaman dan menyortir informasi dari rute itu bagus. Dan mendorongnya ke parameter POST buruk. Halo? Pertanyaannya adalah tentang REST. Kami tidak menggunakan POST hanya untuk membuat URL lebih pendek di REST. Kata kerja masuk akal.
temoto
1
Secara pribadi, saya tidak akan menggunakan parameter formulir untuk kueri karena hampir akan membutuhkan metode POST atau PUT HTTP (karena ada badan sekarang dalam permintaan). Menurut saya GET menyukai metode yang lebih tepat untuk digunakan karena POST dan PUT menyiratkan memodifikasi sumber daya. Karena itu saya akan menambahkan parameter kueri ke URL ketika menyortir dengan beberapa kolom diperlukan.
Paul D. Eden
1

Saya cenderung setuju dengan slf bahwa "halaman" sebenarnya bukan sumber. Di sisi lain, opsi 3 lebih bersih, lebih mudah dibaca, dan dapat lebih mudah ditebak oleh pengguna dan bahkan diketikkan jika perlu. Saya terpecah antara opsi 1 dan 3, tetapi tidak melihat alasan untuk tidak menggunakan opsi 3.

Selain itu, walaupun terlihat bagus, satu kelemahan menggunakan parameter tersembunyi, seperti yang disebutkan seseorang, alih-alih string kueri atau segmen URL adalah bahwa pengguna tidak dapat membookmark atau menautkan langsung ke halaman tertentu. Itu mungkin atau mungkin bukan masalah tergantung pada aplikasi, tetapi hanya sesuatu yang harus diperhatikan.

insane.dreamer
sumber
1
Mengenai penyebutan Anda yang lebih mudah ditebak, ini seharusnya tidak masalah. Jika membuat API hypermedia, pengguna tidak boleh HARUS menebak URI.
JR Garcia
0

Saya telah menggunakan solusi 3 sebelumnya (saya menulis BANYAK aplikasi Django). Dan saya tidak berpikir ada yang salah dengan itu. Ini sama mudahnya dengan dua lainnya (jika Anda perlu melakukan pengikisan massal atau sejenisnya) dan terlihat lebih bersih. Plus, pengguna Anda dapat menebak url (jika ini adalah aplikasi yang menghadap publik), dan orang-orang suka dapat langsung menuju ke tempat yang mereka inginkan, dan menebak-nebak url terasa memberdayakan.

Alex
sumber
0

Saya menggunakan dalam url proyek saya url berikut:

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

yang berarti - "beri saya halaman halaman kedua memerintahkan naik dengan field1 dan kemudian turun dengan field2". Atau jika saya membutuhkan lebih banyak fleksibilitas saya gunakan:

http://application/products?skip=20&limit=20&sort=+field1-field2
Eugene
sumber
0

Saya menggunakan dalam pola berikut untuk mendapatkan catatan halaman berikutnya. http: // aplikasi / produk? lastRecordKey =? & pageSize = 20 & sort = ASC

RecordKey adalah kolom tabel yang menyimpan nilai sekuensial dalam DB. Ini digunakan untuk mengambil hanya satu data halaman sekaligus dari DB. pageSize digunakan untuk menentukan berapa banyak rekaman yang harus diambil. sort digunakan untuk mengurutkan record dalam urutan menaik atau menurun.

Susanta Ghosh
sumber