Praktik terbaik pagination API

288

Saya membutuhkan bantuan menangani case edge yang aneh dengan API paginasi yang saya bangun.

Seperti banyak API, ini memberikan hasil yang besar. Jika Anda query / foos, Anda akan mendapatkan 100 hasil (mis. Foo # 1-100), dan tautan ke / foos? Page = 2 yang akan mengembalikan foo # 101-200.

Sayangnya, jika foo # 10 dihapus dari kumpulan data sebelum konsumen API membuat kueri berikutnya, / foos? Page = 2 akan diimbangi dengan 100 dan mengembalikan foo # 102-201.

Ini adalah masalah bagi konsumen API yang mencoba menarik semua foo - mereka tidak akan menerima foo # 101.

Apa praktik terbaik untuk menangani ini? Kami ingin menjadikannya seringan mungkin (yaitu menghindari sesi penanganan untuk permintaan API). Contoh dari API lain akan sangat dihargai!

2arrs2ells
sumber
1
apa masalahnya di sini? tampaknya baik-baik saja bagi saya, bagaimanapun pengguna akan mendapatkan 100 item.
NARKOZ
2
Saya telah menghadapi masalah yang sama dan mencari solusi. AFAIK, sebenarnya tidak ada mekanisme jaminan yang solid untuk mencapai ini, jika setiap halaman mengeksekusi kueri baru. Satu-satunya solusi yang dapat saya pikirkan adalah menjaga sesi aktif, dan menjaga hasilnya tetap di sisi server, dan alih-alih mengeksekusi query baru untuk setiap halaman, ambil saja rekaman yang di-cache berikutnya.
Jerry Dodge
31
Lihatlah bagaimana twitter mencapai dev.twitter.com/rest/public/timelines
java_geek
1
@java_geek Bagaimana parameter since_id diperbarui? Di halaman web twitter sepertinya mereka membuat kedua permintaan dengan nilai yang sama untuk since_id. Saya ingin tahu kapan akan diperbarui sehingga jika tweet yang lebih baru ditambahkan, mereka dapat diperhitungkan?
Petar
1
@Petar Parameter since_id perlu diperbarui oleh konsumen API. Jika Anda lihat, contoh di sana mengacu pada klien yang memproses tweet
java_geek

Jawaban:

175

Saya tidak sepenuhnya yakin bagaimana data Anda ditangani, jadi ini mungkin atau mungkin tidak berfungsi, tetapi sudahkah Anda mempertimbangkan membuat halaman dengan bidang stempel waktu?

Ketika Anda query / foos Anda mendapatkan 100 hasil. API Anda kemudian harus mengembalikan sesuatu seperti ini (dengan asumsi JSON, tetapi jika perlu XML prinsip-prinsip yang sama dapat diikuti):

{
    "data" : [
        {  data item 1 with all relevant fields    },
        {  data item 2   },
        ...
        {  data item 100 }
    ],
    "paging":  {
        "previous":  "http://api.example.com/foo?since=TIMESTAMP1" 
        "next":  "http://api.example.com/foo?since=TIMESTAMP2"
    }

}

Hanya sebuah catatan, hanya menggunakan satu cap waktu bergantung pada 'batas' implisit dalam hasil Anda. Anda mungkin ingin menambahkan batas eksplisit atau juga menggunakan untilproperti.

Stempel waktu dapat ditentukan secara dinamis menggunakan item data terakhir dalam daftar. Ini sepertinya kurang lebih bagaimana paginasi Facebook dalam API Grafiknya (gulir ke bawah untuk melihat tautan pagination dalam format yang saya berikan di atas).

Satu masalah mungkin jika Anda menambahkan item data, tetapi berdasarkan deskripsi Anda, sepertinya akan ditambahkan ke bagian akhir (jika tidak, beri tahu saya dan saya akan melihat apakah saya dapat memperbaiki ini).

ramblinjan
sumber
29
Stempel waktu tidak dijamin unik. Artinya, banyak sumber daya dapat dibuat dengan stempel waktu yang sama. Jadi pendekatan ini memiliki kelemahan yaitu halaman berikutnya, mungkin mengulangi entri terakhir (beberapa?) Dari halaman saat ini.
rubel
4
@prmatta Sebenarnya, tergantung pada implementasi basis data, stempel waktu dijamin unik .
ramblinjan
2
@jandjorgensen Dari tautan Anda: "Tipe data timestamp hanyalah angka yang bertambah dan tidak mempertahankan tanggal atau waktu. ... Di SQL server 2008 dan yang lebih baru, tipe cap waktu telah diubah namanya menjadi rowversion , mungkin untuk mencerminkan dengan lebih baik tujuan dan nilai. " Jadi tidak ada bukti di sini bahwa cap waktu (yang sebenarnya mengandung nilai waktu) adalah unik.
Nolan Amy
3
@jandjorgensen Saya suka proposal Anda, tetapi tidakkah Anda memerlukan beberapa jenis informasi di tautan sumber daya, jadi kami tahu jika kami pergi sebelumnya atau berikutnya? Seperti: "sebelumnya": " api.example.com/foo?before=TIMESTAMP " "next": " api.example.com/foo?since=TIMESTAMP2 " Kami juga akan menggunakan id urutan kami alih-alih cap waktu. Apakah Anda melihat ada masalah dengan itu?
longliveenduro
5
Pilihan lain yang serupa adalah dengan menggunakan bidang taut
Anthony F
28

Anda memiliki beberapa masalah.

Pertama, Anda memiliki contoh yang Anda kutip.

Anda juga memiliki masalah serupa jika baris dimasukkan, tetapi dalam hal ini pengguna mendapatkan data duplikat (bisa dibilang lebih mudah dikelola daripada data yang hilang, tetapi masih menjadi masalah).

Jika Anda tidak snapshotting set data asli, maka ini hanya fakta kehidupan.

Anda dapat meminta pengguna membuat snapshot eksplisit:

POST /createquery
filter.firstName=Bob&filter.lastName=Eubanks

Yang menghasilkan:

HTTP/1.1 301 Here's your query
Location: http://www.example.org/query/12345

Maka Anda dapat halaman itu sepanjang hari, karena sekarang statis. Ini bisa sangat ringan, karena Anda bisa menangkap kunci dokumen yang sebenarnya daripada seluruh baris.

Jika use case hanya karena pengguna Anda ingin (dan membutuhkan) semua data, maka Anda bisa memberikannya kepada mereka:

GET /query/12345?all=true

dan cukup kirim seluruh kit.

Will Hartung
sumber
1
(Jenis default foos adalah berdasarkan tanggal pembuatan, jadi penyisipan baris tidak menjadi masalah.)
2arrs2ells
Sebenarnya, hanya mengambil kunci dokumen saja tidak cukup. Dengan cara ini Anda harus meminta objek penuh dengan ID ketika pengguna meminta mereka, tetapi mungkin mereka tidak lagi ada.
Scadge
27

Jika Anda memiliki pagination, Anda juga mengurutkan data dengan beberapa kunci. Mengapa tidak membiarkan klien API memasukkan kunci elemen terakhir dari koleksi yang sebelumnya dikembalikan dalam URL dan menambahkan WHEREklausa ke permintaan SQL Anda (atau sesuatu yang setara, jika Anda tidak menggunakan SQL) sehingga hanya mengembalikan elemen-elemen yang kuncinya lebih besar dari nilai ini?

kamilk
sumber
4
Ini bukan saran yang buruk, namun hanya karena Anda mengurutkan berdasarkan nilai tidak berarti itu adalah 'kunci', yaitu unik.
Chris Peacock
Persis. Misalnya, dalam kasus saya, bidang pengurutan adalah tanggal, dan itu jauh dari unik.
Sab Thiru
19

Mungkin ada dua pendekatan tergantung pada logika sisi server Anda.

Pendekatan 1: Ketika server tidak cukup pintar untuk menangani status objek.

Anda dapat mengirim semua id unik yang di-cache ke server, misalnya ["id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] dan parameter boolean untuk mengetahui apakah Anda meminta catatan baru (tarik untuk menyegarkan) atau catatan lama (muat lebih banyak).

Sever Anda harus bertanggung jawab untuk mengembalikan catatan baru (memuat lebih banyak catatan atau catatan baru melalui tarikan untuk menyegarkan) serta id dari catatan yang dihapus dari ["id1", "id2", "id3", "id4", "id5", " id6 "," id7 "," id8 "," id9 "," id10 "].

Contoh: - Jika Anda meminta memuat lebih dari itu permintaan Anda akan terlihat seperti ini: -

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]
}

Sekarang anggaplah Anda meminta catatan lama (muat lebih banyak) dan anggap catatan "id2" diperbarui oleh seseorang dan catatan "id5" dan "id8" dihapus dari server maka respons server Anda akan terlihat seperti ini: -

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

Tetapi dalam hal ini jika Anda memiliki banyak catatan dalam cache lokal misalkan 500, maka string permintaan Anda akan terlalu panjang seperti ini: -

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request
}

Pendekatan 2: Ketika server cukup pintar untuk menangani status objek sesuai tanggal.

Anda dapat mengirim id catatan pertama dan catatan terakhir dan waktu permintaan sebelumnya. Dengan cara ini permintaan Anda selalu kecil bahkan jika Anda memiliki banyak catatan dalam cache

Contoh: - Jika Anda meminta memuat lebih dari itu permintaan Anda akan terlihat seperti ini: -

{
        "isRefresh" : false,
        "firstId" : "id1",
        "lastId" : "id10",
        "last_request_time" : 1421748005
}

Server Anda bertanggung jawab untuk mengembalikan id dari catatan yang dihapus yang dihapus setelah last_request_time serta mengembalikan catatan yang diperbarui setelah last_request_time antara "id1" dan "id10".

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

Tarik Untuk Menyegarkan: -

masukkan deskripsi gambar di sini

Muat lebih banyak

masukkan deskripsi gambar di sini

Mohd Iftekhar Qurashi
sumber
14

Mungkin sulit untuk menemukan praktik terbaik karena sebagian besar sistem dengan API tidak mengakomodasi skenario ini, karena ini merupakan keunggulan ekstrim, atau mereka biasanya tidak menghapus catatan (Facebook, Twitter). Facebook sebenarnya mengatakan setiap "halaman" mungkin tidak memiliki jumlah hasil yang diminta karena penyaringan dilakukan setelah pagination. https://developers.facebook.com/blog/post/478/

Jika Anda benar-benar perlu mengakomodasi kasing tepi ini, Anda perlu "mengingat" di mana Anda tinggalkan. Saran jandjorgensen hampir tepat, tapi saya akan menggunakan bidang yang dijamin unik seperti kunci primer. Anda mungkin perlu menggunakan lebih dari satu bidang.

Mengikuti aliran Facebook, Anda dapat (dan harus) melakukan cache halaman yang sudah diminta dan hanya mengembalikannya dengan baris yang dihapus difilter jika mereka meminta halaman yang sudah mereka minta.

Brent Baisley
sumber
2
Ini bukan solusi yang bisa diterima. Ini memakan waktu dan memori. Semua data yang dihapus bersama dengan data yang diminta harus disimpan dalam memori yang mungkin tidak digunakan sama sekali jika pengguna yang sama tidak meminta entri lagi.
Deepak Garg
3
Saya tidak setuju. Hanya menyimpan ID unik tidak menggunakan banyak memori sama sekali. Anda tidak perlu menyimpan data tanpa batas, hanya untuk "sesi". Ini mudah dengan memcache, cukup atur durasi kedaluwarsa (yaitu 10 menit).
Brent Baisley
memori lebih murah daripada kecepatan jaringan / CPU. Jadi jika membuat halaman sangat mahal (dalam hal jaringan atau intensif CPU), maka hasil caching adalah pendekatan yang valid @DeepakGarg
U Avalos
9

Pagination pada umumnya adalah operasi "pengguna" dan untuk mencegah kelebihan pada komputer dan otak manusia, Anda biasanya memberikan subset. Namun, daripada berpikir bahwa kita tidak mendapatkan seluruh daftar, mungkin lebih baik untuk bertanya apakah itu penting?

Jika diperlukan tampilan pengguliran langsung yang akurat, API REST yang bersifat permintaan / respons tidak cocok untuk tujuan ini. Untuk ini, Anda harus mempertimbangkan WebSockets atau HTML5 Server-Sent Events untuk memberi tahu ujung depan Anda ketika berhadapan dengan perubahan.

Sekarang jika ada kebutuhan untuk mendapatkan snapshot data, saya hanya akan memberikan panggilan API yang menyediakan semua data dalam satu permintaan tanpa pagination. Pikiran Anda, Anda akan memerlukan sesuatu yang akan melakukan streaming output tanpa memuat sementara di memori jika Anda memiliki kumpulan data yang besar.

Untuk kasus saya, saya secara implisit menunjuk beberapa panggilan API untuk memungkinkan mendapatkan seluruh informasi (terutama data tabel referensi). Anda juga dapat mengamankan API ini sehingga tidak akan merusak sistem Anda.

Archimedes Trajano
sumber
8

Opsi A: Paget Keyset dengan Stempel Waktu

Untuk menghindari kekurangan dari pagination offset yang telah Anda sebutkan, Anda dapat menggunakan pagination berbasis keyset. Biasanya, entitas memiliki stempel waktu yang menyatakan waktu kreasi atau modifikasi mereka. Stempel waktu ini dapat digunakan untuk paginasi: Cukup berikan stempel waktu elemen terakhir sebagai parameter kueri untuk permintaan berikutnya. Server, pada gilirannya, menggunakan timestamp sebagai kriteria filter (misalnya WHERE modificationDate >= receivedTimestampParameter)

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757071}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "lastModificationDate": 1512757072,
        "nextPage": "https://domain.de/api/elements?modifiedSince=1512757072"
    }
}

Dengan cara ini, Anda tidak akan kehilangan elemen apa pun. Pendekatan ini harus cukup baik untuk banyak kasus penggunaan. Namun, ingatlah hal-hal berikut:

  • Anda dapat mengalami loop tanpa akhir ketika semua elemen dari satu halaman memiliki stempel waktu yang sama.
  • Anda dapat mengirimkan banyak elemen beberapa kali kepada klien ketika elemen dengan stempel waktu yang sama tumpang tindih dua halaman.

Anda dapat memperkecil kekurangan tersebut dengan meningkatkan ukuran halaman dan menggunakan cap waktu dengan presisi milidetik.

Opsi B: Perpanjangan Keyset Diperpanjang dengan Token Berlanjut

Untuk menangani kekurangan yang disebutkan pada pagination keyset normal, Anda dapat menambahkan offset ke timestamp dan menggunakan apa yang disebut "Token Berlanjut" atau "Kursor". Offset adalah posisi elemen relatif terhadap elemen pertama dengan stempel waktu yang sama. Biasanya, token memiliki format seperti Timestamp_Offset. Itu diteruskan ke klien dalam respons dan dapat dikirim kembali ke server untuk mengambil halaman berikutnya.

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757072}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "continuationToken": "1512757072_2",
        "nextPage": "https://domain.de/api/elements?continuationToken=1512757072_2"
    }
}

Token "1512757072_2" menunjuk ke elemen terakhir halaman dan menyatakan "klien sudah mendapatkan elemen kedua dengan stempel waktu 1512757072". Dengan cara ini, server tahu ke mana harus melanjutkan.

Harap diingat bahwa Anda harus menangani kasus di mana elemen diubah di antara dua permintaan. Ini biasanya dilakukan dengan menambahkan checksum ke token. Checksum ini dihitung atas ID semua elemen dengan cap waktu ini. Jadi kita berakhir dengan format token seperti ini: Timestamp_Offset_Checksum.

Untuk informasi lebih lanjut tentang pendekatan ini lihat posting blog " Paginasi API Web dengan Token Lanjutan ". Kelemahan dari pendekatan ini adalah implementasi yang rumit karena ada banyak kasus sudut yang harus diperhitungkan. Itu sebabnya perpustakaan seperti kelanjutan-token bisa berguna (jika Anda menggunakan bahasa Java / a JVM). Penafian: Saya penulis posting dan penulis pendamping perpustakaan.

Phauer
sumber
4

Saya pikir saat ini api Anda benar-benar merespons seperti seharusnya. 100 catatan pertama pada halaman dalam urutan keseluruhan objek yang Anda pertahankan. Penjelasan Anda memberi tahu bahwa Anda menggunakan semacam nomor pesanan untuk menentukan urutan objek Anda untuk pagination.

Sekarang, jika Anda ingin halaman 2 harus selalu mulai dari 101 dan berakhir pada 200, maka Anda harus membuat jumlah entri pada halaman sebagai variabel, karena mereka dapat dihapus.

Anda harus melakukan sesuatu seperti kodesemu di bawah ini:

page_max = 100
def get_page_results(page_no) :

    start = (page_no - 1) * page_max + 1
    end = page_no * page_max

    return fetch_results_by_id_between(start, end)
mickeymoon
sumber
1
Saya setuju. alih-alih kueri berdasarkan nomor rekaman (yang tidak dapat diandalkan) Anda harus meminta menurut ID. Ubah kueri Anda (x, m) yang berarti "kembalikan ke m rekaman YANG DITETAPI oleh ID, dengan ID> x", maka Anda dapat dengan mudah mengatur x ke id maksimum dari hasil permintaan sebelumnya.
John Henckel
Benar, baik mengurutkan pada id atau jika Anda memiliki beberapa bidang bisnis konkret untuk mengurutkan seperti creation_date dll.
mickeymoon
4

Untuk menambah jawaban ini oleh Kamilk: https://www.stackoverflow.com/a/13905589

Sangat tergantung pada seberapa besar dataset yang sedang Anda kerjakan. Kumpulan data kecil bekerja dengan efektif pada pagination offset tetapi dataset realtime besar memang membutuhkan pagination kursor.

Menemukan artikel yang bagus tentang bagaimana Slack mengembangkan pagination apinya ketika ada set data yang meningkat yang menjelaskan sisi positif dan negatif pada setiap tahap: https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12

Shubham Srivastava
sumber
3

Saya sudah berpikir panjang dan keras tentang ini dan akhirnya berakhir dengan solusi yang akan saya jelaskan di bawah ini. Ini adalah langkah yang cukup besar dalam kompleksitas tetapi jika Anda melakukan langkah ini, Anda akan berakhir dengan apa yang benar-benar Anda cari, yang merupakan hasil deterministik untuk permintaan di masa mendatang.

Contoh Anda dari item yang dihapus hanya ujung gunung es. Bagaimana jika Anda memfilter dengan color=bluetetapi seseorang mengubah warna item di antara permintaan? Mengambil semua item dengan cara yang dapat dipercaya secara andal adalah mustahil ... kecuali ... kami menerapkan riwayat revisi .

Saya sudah menerapkannya dan itu sebenarnya kurang sulit dari yang saya harapkan. Inilah yang saya lakukan:

  • Saya membuat tabel tunggal changelogsdengan kolom ID penambahan otomatis
  • Entitas saya memiliki idbidang, tetapi ini bukan kunci utama
  • Entitas memiliki changeIdbidang yang merupakan kunci utama dan juga kunci asing untuk changelogs.
  • Setiap kali pengguna membuat, update atau menghapus catatan, sistem menyisipkan rekor baru dalam changelogs, meraih id dan wakilnya itu ke baru versi entitas, yang kemudian menyisipkan dalam DB
  • Kueri saya memilih changeId maksimum (dikelompokkan berdasarkan id) dan bergabung sendiri untuk mendapatkan versi terbaru dari semua catatan.
  • Filter diterapkan ke catatan terbaru
  • Bidang negara melacak apakah suatu barang dihapus
  • Max changeId dikembalikan ke klien dan ditambahkan sebagai parameter kueri dalam permintaan berikutnya
  • Karena hanya perubahan baru yang dibuat, setiap tunggal changeIdmewakili snapshot unik dari data yang mendasarinya saat perubahan itu dibuat.
  • Ini berarti bahwa Anda dapat menyimpan hasil permintaan yang memiliki parameter changeIddi dalamnya selamanya. Hasilnya tidak akan pernah kedaluwarsa karena tidak akan pernah berubah.
  • Ini juga membuka fitur menarik seperti rollback / kembali, menyinkronkan cache klien dll. Setiap fitur yang mendapat manfaat dari perubahan riwayat.
Stijn de Witt
sumber
saya bingung. Bagaimana ini memecahkan kasus penggunaan yang Anda sebutkan? (Bidang acak berubah dalam cache dan Anda ingin membatalkan cache)
U Avalos
Untuk setiap perubahan yang Anda lakukan sendiri, Anda cukup melihat responsnya. Server akan memberikan perubahan baru dan Anda menggunakannya dalam permintaan Anda berikutnya. Untuk perubahan lain (dilakukan oleh orang lain), Anda memilih polling perubahan terbaru setiap waktu dan jika lebih tinggi dari perubahan Anda, Anda tahu ada perubahan luar biasa. Atau Anda mengatur beberapa sistem notifikasi (polling panjang. Server push, websockets) yang memperingatkan klien ketika ada perubahan yang luar biasa.
Stijn de Witt
0

Opsi lain untuk Pagination di RESTFul APIs, adalah menggunakan taut Link yang diperkenalkan di sini . Misalnya Github menggunakannya sebagai berikut:

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

Nilai yang mungkin untuk reladalah: pertama, terakhir, berikutnya, sebelumnya . Namun dengan menggunakan Linktajuk, Anda tidak dapat menentukan total_count (jumlah elemen).

adnanmuttaleb
sumber