web-dev-qa-db-ja.com

APIページングのベストプラクティス

私が構築しているページ付けされたAPIを使用して奇妙なEdgeのケースを処理するためのいくつかの助けが欲しいです。

多くのAPIのように、これは大きな結果をページ付けします。/foosを照会すると、100個の結果(foo#1-100)と/ foos?page = 2へのリンクが表示され、foo#101-200が返されます。

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

これは、すべてのフォーラムを引き出そうとしているAPIコンシューマにとって問題です - 彼らはfoo#101を受け取らないでしょう。

これを処理するためのベストプラクティスは何ですか?できるだけ軽量にしたい(つまり、APIリクエストのセッションを処理しないようにする)。他のAPIの例は大歓迎です。

265
2arrs2ells

私はあなたのデータがどのように扱われるのか完全にはわからないので、これはうまくいくかもしれないし、うまくいかないかもしれませんが、あなたはタイムスタンプフィールドでページ分割することを考えましたか?

/ 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つは、データ項目を追加する場合ですが、説明に基づいて、最後に追加されるように聞こえます(そうでない場合は、お知らせください。これを改善できるかどうかを確認します)。

164
ramblinjan

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

最初に、あなたはあなたが引用した例を持っています。

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

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

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

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

そしてただキット全体を送ってください。

27
Will Hartung

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

24
kamilk

サーバー側のロジックによっては、2つの方法があります。

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

["id1"、 "id2"、 "id3"、 "id4"、 "id5"、 "id6"、 "id7"、 "id8"、 "id1"など、キャッシュされたすべてのレコードの一意のIDをサーバーに送信できます。 "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と最後のレコードの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"]
}

更新するにはプル: -

enter image description here

もっと読み込む

enter image description here

17

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

このEdgeのケースに本当に対応する必要がある場合は、中断した場所を「記憶」する必要があります。 jandjorgensenの提案はその場で提案されていますが、主キーのように一意であることが保証されたフィールドを使用します。複数のフィールドを使用する必要があるかもしれません。

Facebookの流れに従って、あなたはすでに要求されたページをキャッシュすることができ(そしてそうすべきです)、それらが既に要求したページを要求するならば削除された行がフィルタリングされたものだけを返します。

14
Brent Baisley

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

正確なライブスクロールビューが必要な場合、本質的に要求/応答であるREST AP​​Iはこの目的にはあまり適していません。そのためには、WebSocketsまたはHTML5 Server-Sent Eventsを考慮して、フロントエンドに変更を扱うときに知らせる必要があります。

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

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

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

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

  • 単一ページのすべての要素に同じタイムスタンプがあると、無限ループに陥ることがあります。
  • 同じタイムスタンプを持つ要素が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のようなトークン形式になります。

このアプローチの詳細については、ブログ記事「 継続トークンによるWeb APIページ付け 」を参照してください。このアプローチの欠点は、考慮しなければならない多くのコーナーケースがあるため、扱いにくい実装です。だからこそ continuation-token のようなライブラリが便利なのです(Java/JVM言語を使っているなら)。免責事項:私は記事の執筆者であり、図書館の共著者です。

5
phauer

私は現在あなたの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)
4
mickeymoon

Kamilkがこの回答に追加するために: https://www.stackoverflow.com/a/13905589

作業しているデータセットのサイズに大きく依存します。小さいデータセットはoffsetページ付けで効果的に機能しますが、大きいリアルタイムデータセットはcursorページ付けを必要とします。

Slackがどの段階でプラスとマイナスを説明するデータセットが増えていくにつれて、そのAPIのページネーションがどう進化したかについての素晴らしい記事を見つけました。 https://slack.engineering /進化するapi-pagination-at-slack-1c1f644f8e12

3

私はこれについて長くそして一生懸命考え、そして最終的に私が以下で説明する解決策になった。それは複雑さにおいてかなり大きなステップアップですが、もしあなたがこのステップを実行するならば、あなたはあなたが本当にしていたものになってしまうでしょう。

削除されたアイテムの例は、氷山の一角にすぎません。もしあなたがcolor=blueでフィルタリングしているのに、リクエストの合間に誰かがアイテムの色を変えたら?すべての項目をページ単位で確実に取得することは不可能です不可能です...ただし...改訂履歴を実装しない限り.

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

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