【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 | 
テーブルの定義
Section titled “テーブルの定義”model Post {  id        Int     @id @default(autoincrement())  title     String  createdAt Date}データを作成する
Section titled “データを作成する”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)データを取得する
Section titled “データを取得する”1ページ目の取得
Section titled “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を使って整形しています。
次のページを取得する
Section titled “次のページを取得する”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を使って整形しています。
前のページを取得する
Section titled “前のページを取得する”現在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にあります。