ページネーションとは、アイテムのリストを返す際に全てを返すにはリストが大きくなりすぎる可能性がある時にリストの一部を返す方法を言う。ここではREST APIでの場合を考える。
ページネーションにはリストの一部
の指定方法によって複数の方式がある。以下では2つ紹介する。
offset型
リストのインデックス番号を利用して部分を指定する方法。前からoffset
番目のアイテムからlimit
個分を返す。検索結果画面によく使われる、ページ番号で飛べるデザインがこのタイプと言える。一般にlimit
には最大値の制限があり、それ以上取得したい場合はoffset
をずらしながら取得していく。
以下に例を示す。
GET /list?offset=3&limit=4
[
"item4", "item5", "item6", "item7"
]
利点と欠点
利点
- 実装が簡単
- SQLのoffset句、limit句がそのまま使える
- リストの好きな位置に飛べる
欠点
-
offset
をずらしながら連続して取得する時、アイテムの追加や削除が起こるとアイテムが重複したりスキップしてしまったりする。 - リストの先頭にリアルタイムでアイテムが追加されていくような場合に、前回取得分以降を取得する、といったことができない。
cursor型
cursor
と呼ばれる文字列をアイテム一つ一つに付与し、cursor
を基準としてlimit
個分アイテムを取得する方法。最初の取得ではcursor
を使わず、先頭からlimit
個分返す。それ以上取得する場合は、前回の取得の際に得られたcursor
を指定することで、そのcursor
より後のアイテムを返すことができる。「続きを表示」ボタンを押すとアイテムが後ろに追加されていくデザインがこのタイプと言える。
最初の取得の例。
GET /list?limit=3
[
{ "cursor": "mnl29u6j", "node": "item1" },
{ "cursor": "40n2klz2", "node": "item2" },
{ "cursor": "20bhjeri", "node": "item3" }
]
その次の取得の例。
GET /list?cursor=20bhjeri&limit=3
[
{ "cursor": "hi46up39", "node": "item4" },
{ "cursor": "qobn3okd", "node": "item5" },
{ "cursor": "klmlej43", "node": "item6" }
]
実装方法
cursor型の実装方法の肝はcursor
にどんな文字列を使うかである。
この方法は一見すると、まずcursor
が一致するアイテムを探し、次にそれ以降のアイテムを返すという処理になり、実装が複雑になるように見える。
しかし、cursor
に使う文字列を工夫することで単純な処理にすることができる。
cursor
にはリストのソート基準となっている値を使うことで処理を単純にする。
リストをページネーションするからにはそのアイテムの順序には意味があるはずである。
つまり一般的に、ページネーションされるリストはアイテムのもつ何らかの値を基準にソートされている。
このソートの基準として使われる値をcursor
に埋め込む。
これによりcursor
以降のアイテムを取得するという処理をSQLでいうwhere句によって実装できる。
具体例として掲示板における投稿を考える。投稿のリストは投稿日時の新しい順に並べて返すとする。
[
{ "time": "2021-06-23T12:31:45", "content": "投稿8" },
{ "time": "2021-06-23T12:27:32", "content": "投稿7" },
{ "time": "2021-06-23T12:21:16", "content": "投稿6" }
]
このアイテムに対するcursor
には、ソート基準であるtime
を用いればよいので、cursor型ページネーションの返答では
[
{ "cursor": "2021-06-23T12:31:45", "node": { "time": "2021-06-23T12:31:45", "content": "投稿8" }},
{ "cursor": "2021-06-23T12:27:32", "node": { "time": "2021-06-23T12:27:32", "content": "投稿7" }},
{ "cursor": "2021-06-23T12:21:16", "node": { "time": "2021-06-23T12:21:16", "content": "投稿6" }}
]
を返す。
クライアントがこの続きのリストを要求する場合、cursor
に2021-06-23T12:21:16
を指定してくるので、SQLは
SELECT * FROM posts WHERE time < "2021-06-23T12:21:16" ORDER BY time DESC LIMIT 3
と書けば続きを取得できる。
実際にはcursor
にはソート基準の値そのものではなく、その値をBASE64等でエンコードしたものが使われる。
これはクライアントがcursor
の値そのものに意味を見出してそれを前提としたコードを書くことを抑制するためである。
cursor型ページネーションを使う意味
前セクションの通り、この方式は実際にはただのアイテムのフィルタリングである。ではcursor
を使う意味とはなんなのか。
それはcursor
を使うことで統一したページネーションの方法を提供しクライアントが扱いやすくすることにある。検索結果画面などアイテムのソート基準を変えることができる場面でも、cursor
を使うことで、クライアントはソート基準によらず同じ手順で次のアイテムを取得できる。
変異型
新着取得
ここまカーソル以降のアイテムを取得する状況で話したが、カーソル以前のアイテムを取得できるように作ることも簡単にできる(where句の不等号を逆にするだけ)。するとリストの先頭にアイテムが追加されてていく場合に、前回取得分からの新着分のみを取得するといったこともできる。実装上はパラメータにcursor
,limit
の他にdirection
を追加してnext
またはprev
を取るようにするとか。
返答の簡略化
これまでリスト内の全てのアイテムについてカーソルの値を返していたが、実際には最後のアイテム(と前項の場合なら最初のアイテム)のカーソルしか使わない。よってAPIの返答を以下のような簡略化した形式とすることがよくある。
{
"first_cursor": "mnl29u6j",
"last_cursor": "20bhjeri",
"items": [
"item1",
"item2",
"item3"
]
}
利点と欠点
利点
- 連続して取得する時、アイテムの追加や削除が起こってもアイテムが重複したりスキップしたりしない。
- リストの先頭にリアルタイムでアイテムが追加されていくような場合に、前回取得分以降を取得するといったことができる(新着取得)。
欠点
- リストを順番に見ていくことしか出来ず、好きな位置には飛べない
- それでも実装は少し面倒
総評というか感想
常にcursor型でよくねと思う。offset型のメリットはリストの好きな位置にランダムアクセスできる点にあるが、そもそも本質的にランダムアクセスが必要な場面が思い浮かばない。よくある検索結果のページ番号で飛べるタイプの画面も、実際の利用では最初のページから順に見ていく使い方しかしない。実際、Google検索もスマホ版だとページ番号では飛べず、続きを表示するボタンしかないし。