
t0mmy
学習履歴詳細
API デザインパターン 21章 ページ分割 読了
やったこと
- API デザインパターン 21章 ページ分割 読了
学んだこと
ポイント
- ページ分割で使用する、以下3つのフィールドの意味
- pageToken
- maxPageSize
- nextPageToken
- maxPageSize で指定した数をキッチリ返すのではなく、ベストエフォートとする理由
- ページトークンの情報は簡単に解析されてはならない。その理由は?
- オフセットとリミットを使用するべきではない理由
学び
- 「ページ分割パターン」によって、レスポンスを分割できる
- ページ分割パターンでは、以下のフィールドを活用する
- pageToken : 分割したページの位置を表す「トークン」情報
- maxPageSize : レスポンスとして要求する、1ページ当たりのデータ数
- nextPageToken : 次の「ページ」を要求するために必要な「トークン」
- maxPageSize で指定した数だけ、ベストエフォートで検索して返す
- これにより、検索にタイムアウト時間を設定できるようになる
- ページトークンから実装の詳細が漏れると、APIの変更が難しくなる
- ページトークンは、簡単に解析できないような値にする
- オフセットとリミットは、「RDBを使用している」という実装の詳細
- 将来RDBをやめた後でも、オフセットとリミットをサポートし続けなければならない
メモ
対象とする問題
APIで扱うリソースサイズや数が大きくなると、一度のAPIリクエストで扱うのが困難になる。
- 総計100Gや10億個のデータを扱うような場合
酷く時間がかかったり、そもそも扱えなかったりする。
こういった場合は、データを分割して提供することになるが、どういったインターフェースとなるか。
広く普及している「ページ分割」パターンについて述べる。
概要
データを「ページ」という単位に分割する。
利用者は、「ページ」単位でAPIリクエストを送信する。
API側は、対応する「ページ」と、利用者が次の「ページ」を要求するために必要な「トークン」と呼ばれるデータを返す。
実装
以下の3つのフィールドを使用する。
- pageToken : 分割したページの位置を表す「トークン」情報
- maxPageSize : レスポンスとして要求する、1ページ当たりのデータ数
- nextPageToken : 次の「ページ」を要求するために必要な「トークン」
nextPageToken に何らかの値が存在する場合、次のページが存在することを示す。
利用者は、レスポンスで受け取った nextPageToken
の値を、リクエストの pageToken
フィールドに設定してリクエストを送信することで、次の「ページ」情報を取得できる。
◆初回リクエスト
{
maxPageSize: xxx
}
◆レスポンス
{
result: [...],
nextPageToken : "hogefuga"
}
◆次ページを取得するリクエスト
{
maxPageSize: xxx,
pageToken : "hogefuga"
}
【ポイント1】 ページサイズ
◆maxPageSize は ベストエフォート
maxPageSize
で設定した値は、ベストエフォート(≒努力目標)。
10
を指定したからと言って、必ず10件返却するわけではない。
努力目標とすることで、タイムアウト時間を設定できるようになる。
これにより、「検索が終わるまで長時間待つ」といった問題を解消できる。
- そして、追加でデータが欲しければ、pageTokenを使って追加でデータを取得できる
maxPageSize
を 100 , タイムアウト を 0.2 秒 に設定。
最大0.2秒間の間でデータを検索
- 0.2秒 以内 100件以上見つかれば、100件返す
- 0.2秒 経っても、82件しか見つからなければ、 見つけた82件だけ返す
- 本当は100件以上あるが、検索に時間がかかりすぎて、0.2 秒という制限時間内では82件しか見つからなかった
- データが82件しかなかった
◆デフォルト値
maxPageSize
には、利用者が驚かない程度に適切なデフォルト値を設定する。
デフォルト値の存在はドキュメントに明記する。
可能な限り他のAPIのデフォルト値でも、同じデフォルト値を設定する(APIの一貫性を担保)。
【ポイント2】ページトークン
カーソルのような動作をする。
◆終了基準
サーバー側は、全データの検索終了を、pageToken
に空の文字列を設定することで表現する。
そして、レスポンスの結果が空かで判断してはならない。
- 最初の0.2秒では0件、次の0.2秒で1件以上ヒットする ... という可能性は十分考えられる
以下のレスポンスを考える。
{
result: [],
nextPageToken: "xxxxxx"
}
これは、「0.2秒検索した結果、何も見つかりませんでした。このトークンを使う事で、続きから検索できます。」という意味になる。
◆不透明性
ページトークンは、サーバー側でリスト順次処理する際に使用する。
そして、ページトークンの実装は利用者から隠匿すること。
-「 { offset : 10}
のBase64値」のような、簡単に解読されるような値ではいけない
簡単に解読されることは、利用者に実装の詳細を漏らすことを意味する。
これにより、利用者が、自身のアプリケーションに実装の詳細を盛り込む可能性が生まれ、API定義を変更しにくくなってしまう。
◆書式
WebAPIで使用する以上、トークンは文字列で扱うことになる。
◆一貫性
途中でデータが変化した場合でも、最新のページを返却できる方法が必要。
この問題に対する簡潔な答えは存在しない。
- ポイントインタイムスナップショットをサポートするDBであれば、この情報をエンコードしてページトークンとして用いることで、強い一貫性を提供できる
他には、以下の手段が考えられる。
- ドキュメントにて、ページ情報が最新ではない可能性を説明しておく
- 最後の検索結果をカーソルとして使用し、次のページトークンで中断した場所から拾い上げるよう実装する
◆有効期限
一般的に、ページトークンの有効期限は設定しない
- 期限切れトークンはリトライしてもらうしかなく、単に利用者に不便を強いるだけ
有効期限を明確にすると、「利用者は、ページング操作をいつまでに行えばよいか予想できる」という利点が存在する。
この場合、一般的なユースケースで十分な、60分程度を有効期限とするのが良い。
【ポイント3】総数
[xxx件中10~20件]
といったUI向けに、全データの総数を返すか。
- データがそこまで多くなく、総数の計算に時間がかからない => 正確な総数を返す
- データが非常に多く、総数の計算に時間がかかる => 最良の推定値を総数として返す
どちらの場合も、総数は、レスポンスのフィールドに設定する。
{ result: [...], pageToken: "xxx", totalResults: n // 総数 }
【ポイント4】リソース内でのページ分割
一つのリソースが巨大化した場合、一つのリソースを分割し、複数回のリクエストに分けて取得する方法が考えられる。
この場合、本項で取り上げているページ分割をそのまま応用できる。
この時、リソースの読み込み処理は強い一貫性を持つこと。
言い換えると、読み込み中にリソースの変更があった場合、読み込みを中断する事。
トレードオフ
◆双方向ページング
ページ分割では、双方向ページングが提供できない。
nextPageToken
を使う事で「次のリソース」を要求することはできるは、前のリソースは要求できない
一部のユースケースを除き、基本的に双方向ページングが必要になる可能性は非常に低い。
双方向ページングを実装する場合、ページ分割の結果をキャッシュすることで対応できる。
任意のウィンドウ
任意の位置のページをリクエストすることはできない。
この機能も、一部のユースケースを除き、基本的には必要になる可能性が非常に低い。
避けるべきパターン : オフセットとリミット
RDBの OFFSET
と LIMIT
に合わせる形で、APIで OFFSET
と LIMIT
フィールドを扱う。
APIの OFFSET
と LIMIT
フィールドの値を、そのままSQLに渡すだけ。
OFFSET
と LIMIT
はRDBの機能。
OFFSET
と LIMIT
を提供すると、RDBから別のデータストアに移行した後も、これらの機能のサポートを続けなくてはならなくなる。
言い換えると、OFFSET
と LIMIT
フィールドは実装の詳細であり、利用者に漏らすべきではない。
扱うこと
- ページ分割により、大きなデータセットを少しずつ利用する方法
- ページ分割により、APIの利用者はどのようにして大きなデータセットを少しずつ利用できるようになるか
- データを構成するページの具体的なサイズ
- APIサービスは、ページングの完了をどのようにして伝えるべきか
- ページトークンの形式はどのように定義するか
- ひとつのリソース内の大きなデータチャンクをページングする方法
まとめ
- ページ分割は、3つの特別なフィールドを使用することで、APIレスポンスを分割し、一口サイズのチャンクで送信できるようになる
- 3つの特別なフィールド :
maxPageSize
,pageToken
,nextPageToken
- 3つの特別なフィールド :
- ページがまだ存在する場合は、
nextPageToken
が何らかの値を持つ- この値の使用方法 : 後続リクエストの
pageToken
フィールドに設定する
- この値の使用方法 : 後続リクエストの
- 以下の理由により、正確なページサイズではなく、最大ページサイズを用いる
- 正確なデータを取得するために必要な時間がわからない
- 完全なデータが手に入る前でも、レスポンスを返す必要がある
- ページングが終了するタイミングは、レスポンスの
nextPageToken
に値が存在しない場合- 結果フィールドが空の時ではない
2023年11月19日(日)
1.5時間