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

ページネーションとは、アイテムのリストを返す際に全てを返すにはリストが大きくなりすぎる可能性がある時にリストの一部を返す方法を言う。ここではREST APIでの場合を考える。

ページネーションには`リストの一部`の指定方法によって複数の方式がある。以下では2つ紹介する。

# offset型
リストのインデックス番号を利用して部分を指定する方法。前から`offset`番目のアイテムから`limit`個分を返す。検索結果画面によく使われる、ページ番号で飛べるデザインがこのタイプと言える。一般に`limit`には最大値の制限があり、それ以上取得したい場合は`offset`をずらしながら取得していく。

以下に例を示す。

```
GET /list?offset=3&limit=4
```

```json
[
    "item4", "item5", "item6", "item7"
]
```

## 利点と欠点
利点

- 実装が簡単
  - SQLのoffset句、limit句がそのまま使える
- リストの好きな位置に飛べる

欠点

- `offset`をずらしながら連続して取得する時、アイテムの追加や削除が起こるとアイテムが重複したりスキップしてしまったりする。
- リストの先頭にリアルタイムでアイテムが追加されていくような場合に、前回取得分以降を取得する、といったことができない。

# cursor型
`cursor`と呼ばれる文字列をアイテム一つ一つに付与し、`cursor`を基準として`limit`個分アイテムを取得する方法。最初の取得では`cursor`を使わず、先頭から`limit`個分返す。それ以上取得する場合は、前回の取得の際に得られた`cursor`を指定することで、その`cursor`より後のアイテムを返すことができる。「続きを表示」ボタンを押すとアイテムが後ろに追加されていくデザインがこのタイプと言える。

最初の取得の例。

```
GET /list?limit=3
```

```json
[
    { "cursor": "mnl29u6j", "node": "item1" },
    { "cursor": "40n2klz2", "node": "item2" },
    { "cursor": "20bhjeri", "node": "item3" }
]
```

その次の取得の例。

```
GET /list?cursor=20bhjeri&limit=3
```

```json
[
    { "cursor": "hi46up39", "node": "item4" },
    { "cursor": "qobn3okd", "node": "item5" },
    { "cursor": "klmlej43", "node": "item6" }
]
```

## 実装方法
cursor型の実装方法の肝は`cursor`にどんな文字列を使うかである。
この方法は一見すると、まず`cursor`が一致するアイテムを探し、次にそれ以降のアイテムを返すという処理になり、実装が複雑になるように見える。
しかし、`cursor`に使う文字列を工夫することで単純な処理にすることができる。

`cursor`にはリストのソート基準となっている値を使うことで処理を単純にする。
リストをページネーションするからにはそのアイテムの順序には意味があるはずである。
つまり一般的に、ページネーションされるリストはアイテムのもつ何らかの値を基準にソートされている。
このソートの基準として使われる値を`cursor`に埋め込む。
これにより`cursor`以降のアイテムを取得するという処理をSQLでいうwhere句によって実装できる。

具体例として掲示板における投稿を考える。投稿のリストは投稿日時の新しい順に並べて返すとする。

```json
[
    { "time": "2021-06-23T12:31:45", "content": "投稿８" },
    { "time": "2021-06-23T12:27:32", "content": "投稿７" },
    { "time": "2021-06-23T12:21:16", "content": "投稿６" }
]
```

このアイテムに対する`cursor`には、ソート基準である`time`を用いればよいので、cursor型ページネーションの返答では

```json
[
    { "cursor": "2021-06-23T12:31:45", "node": { "time": "2021-06-23T12:31:45", "content": "投稿８" }},
    { "cursor": "2021-06-23T12:27:32", "node": { "time": "2021-06-23T12:27:32", "content": "投稿７" }},
    { "cursor": "2021-06-23T12:21:16", "node": { "time": "2021-06-23T12:21:16", "content": "投稿６" }}
]
```
を返す。

クライアントがこの続きのリストを要求する場合、`cursor`に`2021-06-23T12:21:16`を指定してくるので、SQLは

```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の返答を以下のような簡略化した形式とすることがよくある。

```json
{
  "first_cursor": "mnl29u6j",
  "last_cursor": "20bhjeri",
  "items": [
    "item1",
    "item2",
    "item3"
  ]
}
```

## 利点と欠点
利点

- 連続して取得する時、アイテムの追加や削除が起こってもアイテムが重複したりスキップしたりしない。
- リストの先頭にリアルタイムでアイテムが追加されていくような場合に、前回取得分以降を取得するといったことができる(新着取得)。

欠点

- リストを順番に見ていくことしか出来ず、好きな位置には飛べない
- それでも実装は少し面倒

# 総評というか感想
常にcursor型でよくねと思う。offset型のメリットはリストの好きな位置にランダムアクセスできる点にあるが、そもそも本質的にランダムアクセスが必要な場面が思い浮かばない。よくある検索結果のページ番号で飛べるタイプの画面も、実際の利用では最初のページから順に見ていく使い方しかしない。実際、Google検索もスマホ版だとページ番号では飛べず、続きを表示するボタンしかないし。