ページングと並び替え
このレッスンでは、ページ番号と1ページの件数から取り出す範囲を計算できるようになり、Pageable が裏でやっていることを説明できるようになります。
全件返してはいけません
一覧の API で findAll() をそのまま返すのは、データが少ないうちだけ許される書き方です。10万件になった瞬間に、データベースからの転送、Java のヒープ、JSON への変換、クライアント側の描画のすべてが同時に苦しくなります。区切って返す仕組みが要ります。
Spring Data JPA では Pageable を引数に足すだけで対応できます。
Java
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByAgeGreaterThan(int age, Pageable pageable);
}
Pageable pageable = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<User> page = userRepository.findByAgeGreaterThan(18, pageable);Page は取得した中身に加えて、総件数や総ページ数といった情報も持っています。getContent() で中身、getTotalElements() で総件数、getTotalPages() で総ページ数、hasNext() で次のページの有無が取れます。
ページ番号は0始まりです
最初に必ずつまずくのがここです。PageRequest.of(0, 20) が1ページ目、of(1, 20) が2ページ目を指します。画面には「1ページ目」と表示したいので、コントローラで受け取った値から1を引いて渡す、という変換をどこかに置くことになります。この変換を忘れると、利用者から見て2件目のページが最初に出る、という分かりにくい不具合になります。
SQL では OFFSET と LIMIT になります
ページングの正体は、ソート済みの結果から先頭を読み飛ばして必要な数だけ取る、という操作です。
SQL クエリ
SELECT * FROM users ORDER BY name ASC LIMIT 20 OFFSET 40;読み飛ばす件数が OFFSET、取る件数が LIMIT です。ページ番号との関係は次の式になります。
| 値 | 式 |
|---|---|
| offset | ページ番号 × 1ページ件数 |
| limit | 1ページ件数 |
| 総ページ数 | 総件数を1ページ件数で割った切り上げ |
総ページ数の切り上げは、整数のまま (total + size - 1) / size と書くのが定番です。Math.ceil を使うと double が混ざり、件数が大きいときに誤差の心配が出てきます。
ORDER BY を必ず付けるという点も忘れないでください。並び順が決まっていない結果に対して OFFSET を掛けると、データベースは毎回同じ順序を返すとは限りません。1ページ目に出た行が2ページ目にも現れる、といった取りこぼしや重複が起きます。Sort を渡す、あるいはメソッド名に OrderBy を書くのは、見た目を整えるためではなく正しさのためです。
最後のページと範囲外
総件数が1ページ件数で割り切れないとき、最後のページは端数になります。総件数25件で1ページ10件なら、ページ0とページ1は10件ずつ、ページ2は5件です。総ページ数は3です。
さらに、存在しないページ番号を要求されることもあります。3ページしかないのにページ10を求められたら、返る件数は0件です。例外にせず空の結果を返すのが素直な扱いで、Spring の Page も同じ振る舞いをします。総件数が0件の場合、総ページ数は0とするのが Page の規約です。
演習で書くもの
演習では、Pageable から Page が組み立てられるまでの計算を自分で書きます。総件数、ページ番号、1ページの件数を受け取り、読み飛ばす件数、そのページに実際に載る件数、総ページ数の3つを求めて返します。端数の出る最終ページと、範囲外のページ番号がテストに含まれます。
要件
- 戻り値は offset=40,count=10,totalPages=5 の形にすること
- totalPages は切り上げで求め、total が 0 のときは 0 とすること
- count は最終ページの端数を正しく返し、範囲外のページでは 0 とすること
入出力例
page(100, 4, 10) → "offset=40,count=10,totalPages=10"
page(25, 0, 10) → "offset=0,count=10,totalPages=3"
page(25, 2, 10) → "offset=20,count=5,totalPages=3"
page(25, 10, 10) → "offset=100,count=0,totalPages=3"
page(0, 0, 20) → "offset=0,count=0,totalPages=0"
page(7, 1, 7) → "offset=7,count=0,totalPages=1"