6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

REST APIにおけるページネーション

Posted at

ページネーションとは、アイテムのリストを返す際に全てを返すにはリストが大きくなりすぎる可能性がある時にリストの一部を返す方法を言う。ここでは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" }}
]

を返す。

クライアントがこの続きのリストを要求する場合、cursor2021-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検索もスマホ版だとページ番号では飛べず、続きを表示するボタンしかないし。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?