【TypeScript】【Prisma】Cursor-Based Paginationを触ってみる。
はじめに
cusrorを使ったページネーションは、ページネーションの一種で、データベースのクエリ結果やAPIのレスポンスにおいて、特定の位置を指定して結果を取得する方法です。
TypeScript向けのオープンソースのORM(Object Relational Mapping)ライブラリであるPrismaのドキュメントにはoffsetを使ったページネーションとcursorを使ったページネーションが紹介されています。
今回はcursorを使ったページネーションの書き方や、実行されるSQLのクエリを確認します
成果物
https://github.com/kntks/blog-code/tree/main/2023/03/prisma-cursor-based-pagination
バージョン
バージョン | |
---|---|
node | 18.13.0 |
npm | 8.19.3 |
prisma | 4.11.0 |
mysql | 8.0.32 |
準備
テーブルの定義
model Post { id Int @id @default(autoincrement()) title String createdAt Date}
データを作成する
await prisma.$transaction( [...Array(20)].map((_, i) => { return prisma.posts.upsert({ where: { id: i + 1 }, update: {}, create: { id: i + 1, title: `title${i + 1}` }, }); }));
async function main() { await createPosts();}
データを20個作成しました。
mysql> select * from posts;+----+---------+| id | title |+----+---------+| 1 | title1 || 2 | title2 || 3 | title3 || 4 | title4 || 5 | title5 || 6 | title6 || 7 | title7 || 8 | title8 || 9 | title9 || 10 | title10 || 11 | title11 || 12 | title12 || 13 | title13 || 14 | title14 || 15 | title15 || 16 | title16 || 17 | title17 || 18 | title18 || 19 | title19 || 20 | title20 |+----+---------+20 rows in set (0.00 sec)
データを取得する
1ページ目の取得
1ページ目となる先頭5つのデータを取得します。
async function findMany() { return await prisma.posts.findMany({ take: 5, orderBy: [{ id: "asc" }] })}
$ npx ts-node src/main.ts[ { id: 1, title: 'title1' }, { id: 2, title: 'title2' }, { id: 3, title: 'title3' }, { id: 4, title: 'title4' }, { id: 5, title: 'title5' }]
実行されたクエリは以下の通りです。
SELECT `example`.`posts`.`id`, `example`.`posts`.`title`FROM `example`.`posts`WHERE 1 = 1ORDER BY `example`.`posts`.`id` ASCLIMIT ? OFFSET ?
sqlformatterを使って整形しています。
次のページを取得する
Cursor-based pagination - Prisma docsは、cursorにIDやタイムスタンプのようなユニークで連続したカラムを使う必要がある、と言っています。今回の場合、IDをauto incrementにしていているため、cursorはIDで良さそうです。
次のページを取得したい場合は、1ページ目にあった最後のIDをcursorとして使用します。
async function findMany() { return await prisma.posts.findMany({ take: 5, skip: 1, cursor: { id: 5 }, orderBy: [{ id: "asc" }] })}
$ npx ts-node src/main.ts[ { id: 6, title: 'title6' }, { id: 7, title: 'title7' }, { id: 8, title: 'title8' }, { id: 9, title: 'title9' }, { id: 10, title: 'title10' }]
どうやらcursorを設定すると、サブクエリを実行していることがわかります。
SELECT `example`.`posts`.`id`, `example`.`posts`.`title`FROM `example`.`posts`WHERE `example`.`posts`.`id` >= ( SELECT `example`.`posts`.`id` FROM `example`.`posts` WHERE (`example`.`posts`.`id`) = (?) )ORDER BY `example`.`posts`.`id` ASCLIMIT ? OFFSET ?
sqlformatterを使って整形しています。
前のページを取得する
現在3ページ目にいると仮定します。前のページのデータ(id: 5 ~ 10)はどうやって取得したら良いでしょうか?
async function findMany() { return await prisma.posts.findMany({ take: 5, skip: 1, cursor: { id: 10 }, orderBy: [{ id: "asc" }] })}
$ npx ts-node src/main.ts[ { id: 11, title: 'title11' }, { id: 12, title: 'title12' }, { id: 13, title: 'title13' }, { id: 14, title: 'title14' }, { id: 15, title: 'title15' }]
Prismaのドキュメントに書いてありました。取得するサイズ ( take)の値をマイナスにすれば、前のページのデータを取得できそうです。
やってみます。cursorは3ページ目先頭のid: 11です。
async function findMany() { return await prisma.posts.findMany({ take: -5, skip: 1, cursor: { id: 11 }, orderBy: [{ id: "asc" }] })}
無事に2ページ目と同じデータを取得することができました。
$ npx ts-node src/main.ts[ { id: 6, title: 'title6' }, { id: 7, title: 'title7' }, { id: 8, title: 'title8' }, { id: 9, title: 'title9' }, { id: 10, title: 'title10' }]
SQLだと以下のようなクエリが実行されおり、次のページに進んでいた場合と違う箇所はORDERがASC
からDESC
にかわっています。
つまり並び順を昇順から降順にして、takeで指定した数だけ取得しているということですね。
SELECT `example`.`posts`.`id`, `example`.`posts`.`title`FROM `example`.`posts`WHERE `example`.`posts`.`id` <= ( SELECT `example`.`posts`.`id` FROM `example`.`posts` WHERE (`example`.`posts`.`id`) = (?) )ORDER BY `example`.`posts`.`id` DESCLIMIT ? OFFSET ?
sqlformatterを使って整形しています。
図で表現すると以下のようなイメージです。
Previous Page Token for Cursor Paginationにも同じテクニックが書いてありました。
まとめ
prismaでcusor baseのページネーションをするとき以下のことがわかりました
- 次のページに進みたいときはcursorに値を設定し、takeとskipを設定する。
- 前のページに戻りたいときはcursorに値を設定し、takeをマイナスにして設定する。
今回使用したコードは blog-code/prisma-cursor-based-pagination - GitHubにあります。