しかしデータサイズが大きくなれば重くなる。応答速度が極めて重要な場面もあるかもしれない。
そんなとき、Web APIの性能を最適化する方法として利用できるのがPagination、キャッシング、圧縮である。
今回はPaginationについて取り上げる。
Paginationとは
日本語では「ページネーション」と言うようだ。英語発音では「パジネーション」。ここでの表記はPaginationに統一する。
PaginationとはWebの世界でページを分割することを言う。
GoogleやAmazonで検索すると1ページに全ての検索結果が表示されることはなく、適切なサイズにページが分割されて、下の方に次のページへのリンクが表示されるかと思う。あれがPaginationだ。
Web APIにPaginationは必要なのか?
人間が見るサイトならばPaginationが必須なことはすぐに理解できるだろう。適切なPaginationは閲覧性を改善してUser Experienceが向上するだけでなくSEOにも不可欠となっている。しかしマシン同士のやりとりであるWeb APIでPaginationは必要なのだろうか。
データ量の肥大化
昨今、システムが扱うデータの量は肥大化の一歩を辿っている。クライアントが気軽にGETしたらとてつもない量のResourceにHitしてしまう可能性があるということだ。Googleが検索結果の全量をPaginationなしであたなのソフトウェアに送り返してきたときのことを想像してみて欲しい。それはGoogleにとってもあなたのソフトウェアにとってもテロ行為であることは納得できるだろうし、結果を待っているユーザも待ちくたびれてしまう。
実は身近なPagination
今日、WebサービスでもPaginationを使うのが当然となっている。クライアントがなにも指定しなければサーバはサーバの既定値でPaginationを行うのがBest Practiceとされている。決してPaginationなしで結果全量をクライアントに返したりしない。意識していなくてもPaginationを利用しているのだ。
Paginationは必須
PaginationがないWebサービスなどほぼ存在しないと言っていいだろう。サーバはクライアントに適切なPaginationを提供することを期待されている。一方、クライアントはPaginationを適切に利用して迅速に応答を取得・処理するだけでなく、サーバとクライアント双方のリソースを保護することも期待されている。Webサービス開発においてはサーバ開発者もクライアント開発者もPaginationについて理解しておかなくてはならないのだ。
サーバ主導かクライアント主導か
Paginationに適切なサイズをクライントが決めるのとサーバが決めるの、どちらがよいのだろう。
Best Practiceはクライントが決めることである。
デバイスが多様化している現在、サーバにはクライアントに適したサイズが判断できないからだ。もしクライントが指定しなかった場合は仕方がないのでサーバの規定の値を使用する。
方式
Paginationには主に以下の2つの方式がある。
- Page-Based Pagination
- Cursor-Based Pagination
Page-Based Pagination
- 最も基本的な実現方式
- クライアントがページ番号とページサイズを指定する
- ページサイズ指定はオプションとすることが多い
- 2ページ目を閲覧中に1ページ目の項目が追加/削除されると表示がずれてしまうことがデメリット
例1
- 3ページ目を要求。
- ページサイズはサーバの既定値を使用する。
GET /events?page=3
例2
- 3ページ目を要求。
- ページサイズは30エントリ。
GET /events?per_page30&page=3
例3
- 同じく3ページ目を要求。
- ページサイズは30エントリ
- Query Parameterが異なる(SQL文に近い)。
GET /events?limit=30&offset=60
Cursor-Based Pagination
- keyset-basedとも呼ばれる
- 現在位置(cursor)と、現在位置からの表示数を指定する
- 現在位置を認識しているのでPage-based Paginationのデメリットが発生しない
例1
- message id 6より前のmessageを最大3個表示
GET /messages?beforeId=6&max=3
例2
- timestampより新しいmessageを最大2個表示
GET /messages?afterDate=<timestamp>&max=2
Hypermedia as the Engine of Application State
ここまでクライアントがどのようにページ番号とページサイズを指定するかについて見てきた。
ここからはサーバがどのようにHTTP応答にLinkを含めるのか見ていこう。
HATEOASという考え方がある。REST APIは応答の中で関連するResourceへのLinkを示すべきという設計原則だ。この原則によればクライアントはREST APIのEntry Pointだけ知っていればよく、後は応答に含まれるLinkを再帰的に辿ればいずれ目的のResourceに辿り着けなくてはならない。
HATEOASを実現するにはサーバはページの中にLinkを含めなくてはならないし、クライアントはそのLinkを解釈しなくてはならない。
HTTP Link Header
HATEOSに利用できるLink方式の1つがRFC 8288 (Web Linking) で定義されているのがHTTP Link Headerとrel parameterだ。
以下に例を示す。
<head> <link rel=“first” href=”http://www.example.com/page1.html”> <link rel=“prev” href=”http://www.example.com/page4.html”> <link rel=“next” href=”http://www.example.com/page6.html”> <link rel=“last” href=”http://www.example.com/page10.html”> </head>
これは全部で10ページある結果の5ページ目をクライアントが要求したときの応答に含まれるLinkヘッダの例である。
それぞれのLinkがfirst、last、previous、nextのいずれに相当するかrel parameterが示している。このお陰でクライアントは次のリソースのURLを組み立てる必要がない。Linkヘッダを解析して、次に欲しいリソースのURLを次のGET要求にコピーすればよいのだ。Linkヘッダの解析にはrequests.utils.parse_header_linksなどのライブラリが利用できる。これはクライアントの実装を容易にする。
Metadata in Response Body
HATEOSに利用できるもう1つの方式がResponse BodyにMetadataを埋め込むことだ。
以下にその例を示す。
{ "_metadata": { "page": 5, "per_page": 10, "page_count": 10, "record_count": 100, "links": [ {"first": "/messages?page=1}, {"prev": "/messages?page=4"}, {"next": "/messages?page=6}, {"last": "/messages?page=10} ] },
Response Bodyにmetadataを挿入してその中でページ番号、ページサイズ、LinkをJSON形式で記述している。簡単に取得して次のGETに利用出来る点はHTTP Link Headerと同じである。
サーバがRFC8288をサポートしていない場合には選択肢となりうる。