319

私が構築しているページ付けされたAPIを使用して、奇妙なエッジケースを処理するのに役立つ情報が欲しいです。

多くのAPIと同様に、これは大きな結果をもたらします。/ foosをクエリすると、100件の結果(つまり、foo#1-100)と、foo#101-200を返す/ foos?page=2へのリンクが得られます。

残念ながら、APIコンシューマーが次のクエリを実行する前にfoo#10がデータセットから削除された場合、/ foos?page = 2は100だけオフセットされ、foos#102-201を返します。

これは、すべてのfooをプルしようとしているAPIコンシューマーにとって問題です。つまり、foo#101を受信しません。

これを処理するためのベストプラクティスは何ですか?可能な限り軽量化したいと考えています(つまり、APIリクエストのセッションの処理を回避します)。他のAPIの例をいただければ幸いです。

4

12 に答える 12

191

データがどのように処理されるか完全にはわからないため、これが機能する場合と機能しない場合がありますが、タイムスタンプフィールドでページ付けすることを検討しましたか?

/ foosをクエリすると、100件の結果が得られます。APIは次のようなものを返す必要があります(JSONを想定していますが、XMLが必要な場合は、同じ原則に従うことができます)。

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

}

注意点として、1つのタイムスタンプのみを使用すると、結果の暗黙的な「制限」に依存します。明示的な制限を追加するか、untilプロパティを使用することもできます。

タイムスタンプは、リストの最後のデータ項目を使用して動的に決定できます。これは、FacebookがGraph APIでページ分割する方法とほぼ同じようです(下にスクロールして、上記の形式のページ分割リンクを確認してください)。

1つの問題は、データアイテムを追加する場合ですが、説明に基づいて、それらが最後に追加されるように思われます(そうでない場合は、お知らせください。これを改善できるかどうかを確認します)。

于 2012-12-16T21:12:53.993 に答える
31

ページネーションがある場合は、データをキーで並べ替えることもできます。APIクライアントが以前に返されたコレクションの最後の要素のキーをURLに含め、WHERESQLクエリ(またはSQLを使用していない場合は同等のもの)に句を追加して、その要素のみを返すようにしないのはなぜですか?キーはこの値よりも大きいですか?

于 2012-12-16T21:21:52.570 に答える
29

いくつかの問題があります。

まず、引用した例があります。

行が挿入された場合にも同様の問題が発生しますが、この場合、ユーザーは重複データを取得します(データが欠落しているよりも管理が簡単ですが、それでも問題があります)。

元のデータセットのスナップショットを作成していない場合、これは単なる現実です。

ユーザーに明示的なスナップショットを作成させることができます。

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

結果:

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

その後、静的になっているので、一日中ページングできます。行全体ではなく実際のドキュメントキーをキャプチャできるため、これはかなり軽量になる可能性があります。

ユースケースが単にユーザーがすべてのデータを必要としている(そして必要としている)場合は、単にそれをユーザーに与えることができます。

GET /query/12345?all=true

キット全体を送るだけです。

于 2012-12-18T21:27:29.200 に答える
19

サーバー側のロジックに応じて、2つのアプローチがあります。

アプローチ1:サーバーがオブジェクトの状態を処理するのに十分スマートでない場合。

キャッシュされたすべてのレコードの一意のIDをサーバーに送信できます。たとえば、["id1"、 "id2"、 "id3"、 "id4"、 "id5"、 "id6"、 "id7"、 "id8"、 "id9"、 "id10"]と、新しいレコード(プルして更新)または古いレコード(さらにロード)のどちらを要求しているかを知るためのブールパラメーター。

サーバーは、["id1"、 "id2"、 "id3"、 "id4"、 "id5"、 "から削除されたレコードのIDだけでなく、新しいレコード(プルを使用して新しいレコードまたは新しいレコードをロードして更新)を返す責任がありますid6 "、" id7 "、" id8 "、" id9 "、"id10"]。

例:- より多くのロードを要求している場合、要求は次のようになります。-

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

ここで、古いレコードを要求していて(さらにロードする)、「id2」レコードが誰かによって更新され、「id5」および「id8」レコードがサーバーから削除されたとすると、サーバーの応答は次のようになります。

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

ただし、この場合、ローカルにキャッシュされたレコードが多数あるとすると、500とすると、リクエスト文字列は次のように長すぎます。-

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

アプローチ2:サーバーが日付に従ってオブジェクトの状態を処理するのに十分スマートな場合。

最初のレコードと最後のレコードのID、および前の要求のエポック時間を送信できます。このように、キャッシュされたレコードが大量にある場合でも、リクエストは常に小さくなります

例:- より多くのロードを要求している場合、要求は次のようになります。-

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

サーバーは、last_request_timeの後に削除された削除済みレコードのIDを返すだけでなく、「id1」と「id10」の間のlast_request_timeの後に更新されたレコードを返す責任があります。

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

プルして更新:-

ここに画像の説明を入力してください

もっと読み込む

ここに画像の説明を入力してください

于 2015-01-20T10:16:16.617 に答える
15

APIを備えたほとんどのシステムはこのシナリオに対応していないため、ベストプラクティスを見つけるのは難しい場合があります。これは、極端なエッジであるか、通常はレコードを削除しないためです(Facebook、Twitter)。Facebookは実際には、ページネーション後に行われたフィルタリングのために、各「ページ」に要求された結果の数がない可能性があると述べています。 https://developers.facebook.com/blog/post/478/

このエッジケースに本当に対応する必要がある場合は、中断したところを「覚えておく」必要があります。jandjorgensenの提案はほぼ正確ですが、主キーのように一意であることが保証されているフィールドを使用します。複数のフィールドを使用する必要がある場合があります。

Facebookのフローに従って、すでに要求されたページをキャッシュし、すでに要求されたページを要求した場合は、削除された行がフィルターされたページを返すことができます(そしてそうすべきです)。

于 2012-12-16T21:29:17.473 に答える
9

オプションA:タイムスタンプ付きのキーセットページネーション

前述のオフセットページネーションの欠点を回避するために、キーセットベースのページネーションを使用できます。通常、エンティティには、作成時刻または変更時刻を示すタイムスタンプがあります。このタイムスタンプはページネーションに使用できます。最後の要素のタイムスタンプを次のリクエストのクエリパラメータとして渡すだけです。次に、サーバーはタイムスタンプをフィルター基準として使用します(例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"
    }
}

このようにして、要素を見逃すことはありません。このアプローチは、多くのユースケースに十分対応できるはずです。ただし、次の点に注意してください。

  • 1つのページのすべての要素のタイムスタンプが同じである場合、無限ループが発生する可能性があります。
  • 同じタイムスタンプの要素が2ページに重なっている場合は、多くの要素をクライアントに複数回配信できます。

ページサイズを大きくし、ミリ秒の精度でタイムスタンプを使用することで、これらの欠点の可能性を低くすることができます。

オプションB:継続トークンを使用した拡張キーセットページネーション

通常のキーセットページネーションの前述の欠点を処理するために、タイムスタンプにオフセットを追加し、いわゆる「継続トークン」または「カーソル」を使用できます。オフセットは、同じタイムスタンプを持つ最初の要素に対する要素の位置です。通常、トークンの形式は。のようなものTimestamp_Offsetです。応答でクライアントに渡され、次のページを取得するためにサーバーに送信することができます。

{
    "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」はページの最後の要素を指し、「クライアントはタイムスタンプ1512757072の2番目の要素をすでに取得しています」と示しています。このようにして、サーバーは続行する場所を認識します。

2つのリクエスト間で要素が変更された場合を処理する必要があることに注意してください。これは通常、トークンにチェックサムを追加することによって行われます。このチェックサムは、このタイムスタンプを持つすべての要素のIDに対して計算されます。したがって、次のようなトークン形式になりますTimestamp_Offset_Checksum

このアプローチの詳細については、ブログ投稿「継続トークンを使用したWebAPIページネーション」を確認してください。このアプローチの欠点は、考慮しなければならない多くのコーナーケースがあるため、実装が難しいことです。そのため、継続トークンなどのライブラリが便利です(Java / JVM言語を使用している場合)。免責事項:私は投稿の著者であり、ライブラリの共著者です。

于 2017-12-20T12:02:01.740 に答える
8

ページ付けは一般に「ユーザー」操作であり、コンピューターと人間の脳の両方で過負荷を防ぐために、通常はサブセットを提供します。ただし、リスト全体が得られないと考えるよりも、重要かどうかを尋ねた方がよい場合があります。

正確なライブスクロールビューが必要な場合、本質的に要求/応答であるRESTAPIはこの目的にはあまり適していません。このためには、WebSocketまたはHTML5サーバー送信イベントを検討して、変更を処理するときにフロントエンドに通知する必要があります。

データのスナップショットを取得する必要がある場合は、ページネーションなしで1つのリクエストですべてのデータを提供するAPI呼び出しを提供するだけです。大きなデータセットがある場合は、出力を一時的にメモリにロードせずに、出力のストリーミングを実行するものが必要になります。

私の場合、すべての情報(主に参照テーブルデータ)を取得できるように、いくつかのAPI呼び出しを暗黙的に指定します。これらのAPIを保護して、システムに害を及ぼさないようにすることもできます。

于 2015-07-29T19:25:10.253 に答える
6

Kamilkによるこの回答に追加するだけです:https ://www.stackoverflow.com/a/13905589

作業しているデータセットの大きさに大きく依存します。小さなデータセットはオフセットページネーションで効果的に機能しますが、大きなリアルタイムデータセットはカーソルページネーションを必要とします。

データセットが増加し、すべての段階でポジティブとネガティブを説明するにつれて、SlackがAPIのページ付けをどのように進化させたかについての素晴らしい記事を見つけました: https ://slack.engineering/evolveing-api-pagination-at-slack-1c1f644f8e12

于 2018-04-02T17:35:51.080 に答える
4

現在、APIは実際に適切に応答していると思います。ページの最初の100レコードは、管理しているオブジェクトの全体的な順序です。あなたの説明は、ページネーションのためのオブジェクトの順序を定義するために、ある種の順序付けIDを使用していることを示しています。

ここで、ページ2を常に101から開始し、200で終了する必要がある場合は、ページのエントリ数を可変として作成する必要があります。これらは削除される可能性があるためです。

次の擬似コードのようなことをする必要があります。

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)
于 2015-02-05T11:16:37.673 に答える
3

私はこれについて長く懸命に考え、最終的には以下で説明する解決策に行き着きました。これは複雑さのかなり大きなステップですが、このステップを実行すると、最終的には本当に求めているものになります。これは、将来の要求に対する決定論的な結果です。

削除されるアイテムの例は、氷山の一角にすぎません。フィルタリングしているcolor=blueのに、リクエストの合間に誰かがアイテムの色を変更した場合はどうなりますか?ページ形式ですべてのアイテムを確実にフェッチすることは不可能です...改訂履歴を実装しない限り...。

私はそれを実装しました、そしてそれは実際に私が予想したよりも難しくありません。これが私がしたことです:

  • changelogs自動インクリメントID列を持つ単一のテーブルを作成しました
  • 私のエンティティにはidフィールドがありますが、これは主キーではありません
  • エンティティにはchangeId、主キーと変更ログの外部キーの両方であるフィールドがあります。
  • ユーザーがレコードを作成、更新、または削除するたびに、システムはに新しいレコードを挿入しchangelogs、IDを取得して新しいバージョンのエンティティに割り当て、それをDBに挿入します。
  • 私のクエリは、最大のchangeId(idでグループ化)を選択し、それを自己結合して、すべてのレコードの最新バージョンを取得します。
  • フィルタは最新のレコードに適用されます
  • 状態フィールドは、アイテムが削除されたかどうかを追跡します
  • 最大changeIdがクライアントに返され、後続のリクエストでクエリパラメータとして追加されます
  • 新しい変更のみが作成されるため、すべてchangeIdの変更は、変更が作成された時点での基になるデータの一意のスナップショットを表します。
  • changeIdこれは、パラメータを含むリクエストの結果を永久にキャッシュできることを意味します。結果は変更されないため、結果が期限切れになることはありません。
  • これにより、ロールバック/復帰、クライアントキャッシュの同期などのエキサイティングな機能も開きます。変更履歴の恩恵を受ける機能。
于 2017-04-04T23:58:25.513 に答える
2

RESTFul APIでのページネーションの別のオプションは、ここで紹介したLinkヘッダーを使用することです。たとえば、Githubは次のように使用します。

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"

可能な値はrelfirst、last、next、previousです。ただし、ヘッダーを使用すると、 total_count(要素の総数)Linkを指定できない場合があります。

于 2019-12-26T11:14:40.470 に答える
2

API Pagination Designを参照してください。カーソルを使用して、PaginationAPIを設計できます。

彼らはカーソルと呼ばれるこの概念を持っています—それは行へのポインタです。したがって、データベースに「その行の100行後に返してください」と言うことができます。また、インデックス付きのフィールドで行を識別する可能性が高いため、データベースでの実行ははるかに簡単です。そして突然、それらの行をフェッチしてスキップする必要がなくなり、それらを直接通過します。例:

  GET /api/products
  {"items": [...100 products],
   "cursor": "qWe"}

APIは(不透明な)文字列を返します。この文字列を使用して、次のページを取得できます。

GET /api/products?cursor=qWe
{"items": [...100 products],
 "cursor": "qWr"}

実装に関しては、多くのオプションがあります。一般に、製品IDなどの注文基準があります。この場合、いくつかのリバーシブルアルゴリズム(たとえば)を使用して製品IDをエンコードしますhashids。そして、カーソルでリクエストを受信すると、それをデコードして、のようなクエリを生成しますWHERE id > :cursor LIMIT 100

アドバンテージ:

  • dbのクエリパフォーマンスは、cursor
  • クエリ中に新しいコンテンツがdbに挿入されたときに適切に処理する

不利益:

  • previous pageステートレスAPIでリンクを生成することは不可能です
于 2020-12-30T12:41:01.190 に答える