API sayfalama en iyi uygulamaları


288

Yaptığım sayfalandırılmış bir API ile garip bir kenar dava işleme bazı yardım isterim.

Birçok API gibi, bu da büyük sonuçları sayfalandırır. / Foos sorgusunu kullanırsanız, 100 sonuç alırsınız (yani foo # 1-100) ve / foos? Page = 2 bağlantısına yönlendirilirsiniz, bu da foo # 101-200 değerini döndürür.

Ne yazık ki, API tüketicisi bir sonraki sorguyu yapmadan önce foo # 10 veri kümesinden silinirse, / foos? Page = 2 100 ile dengelenir ve # 102-201 foos döndürülür.

Bu, tüm foo'ları çekmeye çalışan API tüketicileri için bir sorundur - 101 # foo almayacaklar.

Bununla başa çıkmanın en iyi yolu nedir? Bunu olabildiğince hafif hale getirmek istiyoruz (yani API istekleri için oturumları işlemekten kaçınmak). Diğer API'lerden örnekler çok takdir edilecektir!


1
burada sorun ne? Bana iyi görünüyor, her iki şekilde de kullanıcı 100 ürün alacak.
NARKOZ

2
Aynı sorunu yaşıyorum ve bir çözüm arıyordum. AFAIK, her sayfa yeni bir sorgu yürütürse, bunu gerçekleştirmek için gerçekten sağlam bir garanti mekanizması yoktur. Aklıma gelen tek çözüm, aktif bir oturum tutmak ve sonucu sunucu tarafında tutmak ve her sayfa için yeni sorgular yürütmek yerine, bir sonraki önbelleğe alınmış kayıt kümesini almak.
Jerry Dodge


1
@java_geek since_id parametresi nasıl güncellenir? Twitter web sayfasında, since_id için her iki isteği de aynı değere sahip gibi görünüyorlar. Acaba ne zaman güncellenecek, böylece daha yeni tweetler eklenirse, bunlar açıklanabilir mi?
Petar

1
@Petar since_id parametresinin API tüketicisi tarafından güncellenmesi gerekir. Görüyorsanız, oradaki örnek, tweetleri işleyen müşterilere atıfta bulunmaktadır
java_geek

Yanıtlar:


176

Verilerinizin nasıl işlendiğinden tam olarak emin değilim, bu işe yarayabilir veya çalışmayabilir, ancak bir zaman damgası alanıyla sayfalandırmayı düşündünüz mü?

/ Foos'u sorguladığınızda 100 sonuç alırsınız. API'nız böyle bir şey döndürmelidir (JSON varsayarsak, ancak XML gerekiyorsa aynı ilkeler takip edilebilir):

{
    "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"
    }

}

Sadece bir not, yalnızca bir zaman damgası kullanmak sonuçlarınızda örtük bir 'sınıra' bağlıdır. Açık bir sınır eklemek veya bir untilözellik kullanmak isteyebilirsiniz .

Zaman damgası, listedeki son veri öğesi kullanılarak dinamik olarak belirlenebilir. Bu, Facebook'un Grafik API'sında nasıl sayfa oluşturduğuna az çok benziyor (yukarıda verdiğim biçimde sayfalandırma bağlantılarını görmek için aşağıya doğru kaydırın).

Bir veri öğesi eklerseniz bir sorun olabilir, ancak açıklamanıza dayanarak sonuna eklenecek gibi görünüyor (eğer değilse, bana bildirin ve bu konuda iyileştirip iyileştiremeyeceğimi göreceğim).


30
Zaman damgalarının benzersiz olduğu garanti edilmez. Yani, aynı zaman damgasıyla birden fazla kaynak oluşturulabilir. Dolayısıyla, bu yaklaşımın bir sonraki sayfanın son (birkaç?) Girişleri geçerli sayfadan tekrarlayabileceği dezavantajı vardır.
ruble

4
@prmatta Aslında, veritabanı uygulamasına bağlı olarak bir zaman damgasının benzersiz olduğu garanti edilir .
ramblinjan

2
@jandjorgensen Bağlantınızdan: "Zaman damgası veri türü yalnızca artan bir sayıdır ve bir tarihi veya saati korumaz. ... SQL Server 2008 ve sonrasında, zaman damgası türü , muhtemelen amaç ve değer. " Dolayısıyla burada zaman damgalarının (aslında bir zaman değeri içerenler) benzersiz olduğuna dair bir kanıt yoktur.
Nolan Amy

3
@jandjorgensen Teklifinizi seviyorum, ancak kaynak bağlantılarında bir tür bilgiye ihtiyacınız olmayacak mı? Sth like: "previous": " api.example.com/foo?before=TIMESTAMP " "next": " api.example.com/foo?since=TIMESTAMP2 " Zaman damgası yerine sekans kimliklerimizi de kullanırız. Bununla ilgili herhangi bir sorun görüyor musun?
longliveenduro

5
Başka bir benzer seçenek, RFC 5988'de
Anthony F

28

Birkaç sorununuz var.

İlk olarak, belirttiğiniz örneğe sahipsiniz.

Satırlar eklenirse de benzer bir sorun yaşarsınız, ancak bu durumda kullanıcı yinelenen veriler alır (kayıp verilerden tartışılması muhtemelen daha kolaydır, ancak yine de bir sorun).

Orijinal veri kümesinin anlık görüntüsünü almıyorsanız, bu sadece bir hayat gerçeğidir.

Kullanıcının açık bir anlık görüntü oluşturmasını sağlayabilirsiniz:

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

Hangi sonuçlar:

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

O zaman, artık statik olduğu için gün boyu sayfalayabilirsiniz. Bu, tüm satırlar yerine gerçek belge anahtarlarını yakalayabileceğiniz için makul derecede hafif olabilir.

Kullanım örneği, kullanıcılarınızın tüm verileri istemesi (ve buna ihtiyaç duyması) ise, bunları onlara verebilirsiniz:

GET /query/12345?all=true

ve tüm kiti gönderin.


1
(Varsayılan tür foos oluşturma tarihine göre olduğundan, satır ekleme bir sorun değildir.)
2arrs2ells 19:12

Aslında, sadece belge anahtarlarını yakalamak yeterli değildir. Bu şekilde kullanıcı istediği zaman tüm nesneleri kimliğe göre sorgulamanız gerekir, ancak artık mevcut olmayabilirler.
Scadge

27

Sayfalandırmanız varsa, verileri bir tuşa göre de sıralayabilirsiniz. API istemcilerinin URL'ye önceden döndürülen koleksiyonun son öğesinin anahtarını eklemesine WHEREve SQL sorgunuza (veya SQL kullanmıyorsanız eşdeğer bir şeye) bir cümle eklemesine izin vermeyin , böylece yalnızca Anahtar bu değerden daha büyük mü?


4
Bu kötü bir öneri değildir, ancak bir değere göre sıralamanız, bunun bir 'anahtar', yani benzersiz olduğu anlamına gelmez.
Chris Peacock

Kesinlikle. Örneğin, benim durumumda, sıralama alanı bir tarih olur ve benzersiz olmaktan uzaktır.
Sat Thiru

19

Sunucu tarafı mantığınıza bağlı olarak iki yaklaşım olabilir.

Yaklaşım 1: Sunucu nesne durumlarını işleyecek kadar akıllı olmadığında.

Önbelleğe alınmış tüm benzersiz benzersiz kimlikleri sunucuya gönderebilirsiniz, örneğin ["id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] ve yeni kayıtlar (yenilemek için çekin) veya eski kayıtlar (daha fazla yükle) isteyip istemediğinizi bilmek için bir boole parametresi.

Sunucunuz yeni kayıtları (daha fazla kayıt veya yenileme için yeni kayıtları yükle) ve ayrıca ["id1", "id2", "id3", "id4", "id5", " Parça tanıtımı6" , "ID7", "ID8", "id9", "ID10"].

Örnek: - Daha fazla yük talep ediyorsanız, talebiniz aşağıdaki gibi görünmelidir: -

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

Şimdi eski kayıtları talep ettiğinizi (daha fazla yükle) ve "id2" kaydının biri tarafından güncellendiğini ve "id5" ve "id8" kayıtlarının sunucudan silindiğini varsayalım: Sunucu yanıtınız şöyle görünmelidir: -

{
        "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"]
}

Ancak, bu durumda çok sayıda yerel önbellek kaydınız varsayalım 500, istek dizeniz şu şekilde çok uzun olacaktır: -

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

Yaklaşım 2: Sunucu, tarihe göre nesne durumlarını işleyecek kadar akıllı olduğunda.

İlk kaydın ve son kaydın ve önceki istek çağının zamanının kimliğini gönderebilirsiniz. Bu şekilde, büyük miktarda önbelleğe alınmış kayıtlarınız olsa bile isteğiniz her zaman küçük olur

Örnek: - Daha fazla yük talep ediyorsanız, talebiniz aşağıdaki gibi görünmelidir: -

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

Sunucunuz, last_request_time öğesinden sonra silinen silinmiş kayıtların kimliklerini ve "id1" ile "id10" arasında last_request_time öğesinden sonra güncellenmiş kaydı döndürmekle sorumludur.

{
        "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"]
}

Tazelemek için çek:-

resim açıklamasını buraya girin

Daha fazla yükle

resim açıklamasını buraya girin


14

API'leri olan çoğu sistem bu senaryoya uymadığından, en iyi uygulamaları bulmak zor olabilir, çünkü aşırı bir avantajdır veya genellikle kayıtları silmezler (Facebook, Twitter). Facebook aslında her bir "sayfanın" sayfalandırma işleminden sonra yapılan filtreleme nedeniyle istenen sayıda sonuç bulunmayabileceğini söylüyor. https://developers.facebook.com/blog/post/478/

Bu kenar kasayı gerçekten yerleştirmeniz gerekiyorsa, kaldığınız yeri "hatırlamanız" gerekir. jandjorgensen önerisi hemen hemen üzerinde, ancak birincil anahtar gibi benzersiz olduğu garanti edilen bir alan kullanırdım. Birden fazla alan kullanmanız gerekebilir.

Facebook'un akışını takiben, zaten talep edilen sayfaları önbelleğe alabilir (ve etmelisiniz) ve daha önce istedikleri bir sayfayı talep etmeleri durumunda silinen satırları filtrelenmiş olanları iade edebilirsiniz.


2
Bu kabul edilebilir bir çözüm değildir. Oldukça zaman ve hafıza tüketiyor. İstenen verilerle birlikte silinen tüm verilerin hafızada tutulması gerekir; bu, aynı kullanıcı daha fazla giriş istemezse kullanılmayabilir.
Deepak Garg

3
Katılmıyorum. Sadece benzersiz kimlikleri tutmak çok fazla bellek kullanmaz. Verileri süresiz olarak tutmazsınız, sadece "oturum" için. Bu memcache ile kolaydır, sadece son kullanma süresini ayarlayın (yani 10 dakika).
Brent Baisley

bellek ağ / CPU hızından daha ucuzdur. Bu nedenle, bir sayfa oluşturmak çok pahalıysa (ağ açısından veya CPU yoğunlukluysa), sonuçları önbelleğe almak geçerli bir yaklaşımdır @DeepakGarg
U Avalos

9

Sayfalandırma genellikle bir "kullanıcı" işlemidir ve hem bilgisayarlarda hem de insan beyninde aşırı yüklenmeyi önlemek için genellikle bir alt küme verirsiniz. Ancak, tüm listeyi almadığımızı düşünmektense sormak daha iyi olabilir mi?

Doğru bir canlı kaydırma görünümü gerekiyorsa, istek / yanıt niteliğinde olan REST API'leri bu amaç için uygun değildir. Bunun için, değişikliklerle uğraşırken kullanıcı arabiriminize bildirmek için WebSockets veya HTML5 Sunucu Gönderilmiş Etkinlikler'i dikkate almalısınız.

Şimdi verilerin anlık bir görüntüsünü almak için bir ihtiyaç varsa , ben sadece hiçbir istek sayfalama ile tek bir istekte tüm verileri sağlayan bir API çağrısı sağlar. Dikkat edin, büyük bir veri kümeniz varsa, çıkışı geçici olarak belleğe yüklemeden akış gerçekleştirecek bir şeye ihtiyacınız olacaktır.

Benim durumum için dolaylı olarak tüm bilgileri (öncelikle referans tablo verileri) almak için bazı API çağrıları belirlemek. Bu API'ları sisteminize zarar vermeyecek şekilde de güvence altına alabilirsiniz.


8

Seçenek A: Zaman Damgasıyla Tuş Takımı Sayfalandırması

Bahsettiğiniz ofset sayfa numarasının dezavantajlarından kaçınmak için tuş takımı tabanlı sayfa numaralandırmayı kullanabilirsiniz. Genellikle, varlıkların oluşturma veya değiştirme zamanlarını belirten bir zaman damgası vardır. Bu zaman damgası sayfalama için kullanılabilir: Son öğenin zaman damgasını sonraki istek için sorgu parametresi olarak iletmeniz yeterlidir. Sunucu da zaman damgasını filtre kriteri olarak kullanır (örn. 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"
    }
}

Bu şekilde, hiçbir unsuru kaçırmazsınız. Bu yaklaşım birçok kullanım durumu için yeterince iyi olmalıdır. Ancak aşağıdakileri aklınızda bulundurun:

  • Tek bir sayfanın tüm öğeleri aynı zaman damgasına sahip olduğunda sonsuz döngülerle karşılaşabilirsiniz.
  • Aynı zaman damgasına sahip öğeler iki sayfayla çakıştığında, birçok öğeyi istemciye birden çok kez gönderebilirsiniz.

Sayfa boyutunu artırarak ve milisaniye hassasiyetinde zaman damgalarını kullanarak bu dezavantajları daha az olası hale getirebilirsiniz.

Seçenek B: Devam Jetonu ile Genişletilmiş Tuş Takımı Sayfalandırması

Normal tuş takımı sayfalamasının belirtilen dezavantajlarını ele almak için zaman damgasına bir ofset ekleyebilir ve "Devam Jetonu" veya "İmleç" kullanabilirsiniz. Ofset, elemanın aynı zaman damgasına sahip ilk öğeye göre konumudur. Genellikle, jetonun bir biçimi vardır Timestamp_Offset. Yanıt olarak istemciye iletilir ve bir sonraki sayfayı almak için sunucuya geri gönderilebilir.

{
    "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"
    }
}

"1512757072_2" jetonu sayfanın son öğesini işaret eder ve "istemcinin zaten zaman damgası 1512757072 olan ikinci öğeyi aldığını" belirtir. Bu şekilde sunucu nereye devam edeceğini bilir.

İki istek arasında öğelerin değiştirildiği durumları ele almanız gerektiğini lütfen unutmayın. Bu genellikle jetona bir sağlama toplamı eklenerek yapılır. Bu sağlama toplamı, bu zaman damgasına sahip tüm öğelerin kimlikleri üzerinden hesaplanır. Sonuç olarak şöyle bir token formatı elde ediyoruz:Timestamp_Offset_Checksum .

Bu yaklaşım hakkında daha fazla bilgi için " Devam Belirteçleri ile Web API Sayfalandırması " adlı blog yazısına göz atın . Bu yaklaşımın bir dezavantajı, dikkate alınması gereken birçok köşe vakası olduğu için zor bir uygulamadır. Bu nedenle, devam belirteci gibi kitaplıklar kullanışlı olabilir (Java / JVM dili kullanıyorsanız). Feragatname: Yazının yazarı ve kütüphanenin ortak yazarıyım.


4

Şu anda apiğinizin aslında olması gerektiği gibi yanıt verdiğini düşünüyorum. Sayfadaki ilk 100 kayıt, bakımını yaptığınız nesnelerin toplam sırasıyla. Açıklamanız, nesnelerinizin sayfa numaralandırma sırasını tanımlamak için bir tür sipariş kimliği kullandığınızı belirtir.

Şimdi, 2. sayfanın her zaman 101'den başlayıp 200'de bitmesini istiyorsanız, sayfadaki giriş sayısını silmeye tabi olduğundan değişken olarak yapmanız gerekir.

Aşağıdaki sözde kod gibi bir şey yapmalısınız:

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)

1
Katılıyorum. (güvenilir olmayan) kayıt numarasına göre sorgulamak yerine kimlikle sorgulamanız gerekir. Sorgunuzu (x, m) "ID ile SIRALANMIŞ m kayıtlara dön, ID> x ile" olarak değiştirin, ardından x'i önceki sorgu sonucundan maksimum kimliğe ayarlayabilirsiniz.
John Henckel

Doğru, ya sıralama kimlikleri üzerinde veya üzerinde tür bazı somut iş alanınız varsa creation_date vb
mickeymoon

4

Bu yanıta Kamilk tarafından eklemek için: https://www.stackoverflow.com/a/13905589

Ne kadar büyük veri kümesi üzerinde çalıştığınıza çok bağlıdır. Küçük veri kümeleri, ofset sayfalama üzerinde etkili bir şekilde çalışır, ancak büyük gerçek zamanlı veri kümeleri imleç sayfalama gerektirir .

Nasıl bir güzel yazıyı Bulunan Gevşek orada veri kümeleri her aşamada pozitif ve negatif açıklayan arttıkça onun API en sayfalama gelişti: https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12


3

Ben bu konuda uzun ve zor düşündüm ve sonunda aşağıda tarif edeceğim çözüm ile sona erdi. Karmaşıklık açısından oldukça büyük bir adımdır, ancak bu adımı yaparsanız, gerçekten peşinde olduğunuz şeyle sonuçlanırsınız, bu da gelecekteki talepler için belirleyici sonuçlardır.

Silinecek bir öğe örneği buzdağının sadece görünen kısmıdır. Filtreleme uygulayan color=blueancak birileri istekler arasında öğe renklerini değiştirirse ne olur ? Tüm öğeleri disk belleği biçiminde güvenilir bir şekilde getirmek imkansızdır ... sürece ... düzeltme geçmişini uygulamazsak .

Uyguladım ve aslında beklediğimden daha az zor. İşte yaptım:

  • changelogsOtomatik artan kimlik sütununa sahip tek bir tablo oluşturdum
  • Varlıklarımın id alanı var, ancak bu birincil anahtar değil
  • Varlıkların changeIdhem birincil anahtar hem de değişim günlükleri için yabancı anahtar olan bir alanı vardır.
  • Bir kullanıcı bir kayıt oluşturduğunda, güncellediğinde veya sildiğinde, sistem yeni bir kayıt ekler changelogs, kimliği yakalar ve varlığın yeni bir sürümüne atar.
  • Sorgularım maksimum changeId (kimliğe göre gruplandırılmış) seçer ve tüm kayıtların en son sürümlerini almak için buna katılır.
  • Filtreler en son kayıtlara uygulanır
  • Durum alanı, bir öğenin silinip silinmediğini izler
  • Max changeId istemciye döndürülür ve sonraki isteklerde bir sorgu parametresi olarak eklenir
  • Çünkü sadece yeni değişiklikler changeId andaki temel verilerin benzersiz bir anlık görüntüsünü temsil eder.
  • Bu, içinde parametre changeIdbulunan isteklerin sonuçlarını sonsuza kadar önbelleğe alabileceğiniz anlamına gelir . Sonuçlar asla sona ermeyecek çünkü asla değişmeyecekler.
  • Bu ayrıca geri alma / geri alma, istemci önbelleğini senkronize etme gibi heyecan verici bir özellik açar. Değişiklik geçmişinden yararlanan tüm özellikler.

kafam karıştı. Bu, bahsettiğiniz kullanım durumunu nasıl çözer? (Önbellekte rastgele bir alan değişir ve önbelleği geçersiz kılmak istersiniz)
U Avalos

Kendiniz yaptığınız herhangi bir değişiklik için yanıta bakarsınız. Sunucu yeni bir changeId sağlar ve bunu bir sonraki isteğinizde kullanırsınız. Diğer değişiklikler için (diğer insanlar tarafından yapılan), en son değişikliği ara sıra yoklarsınız ve eğer kendinizinkinden daha yüksekse, olağanüstü değişiklikler olduğunu bilirsiniz. Veya olağanüstü değişiklikler olduğunda istemciyi uyaran bir bildirim sistemi (uzun yoklama. Sunucu push, websockets) ayarladınız.
Stijn de Witt

0

RESTFul API'lerinde Sayfalandırma için başka bir seçenek, burada tanıtılan Bağlantı başlığını kullanmaktır . Örneğin Github aşağıdaki gibi kullanın :

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"

Olası değerler relşunlardır: ilk, son, sonraki, önceki . Ancak Linküstbilgiyi kullanarak total_count (toplam öğe sayısı) belirtmek mümkün olmayabilir .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.