Skip to content

【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

バージョン

バージョン
node18.13.0
npm8.19.3
prisma4.11.0
mysql8.0.32

準備

テーブルの定義

prisma/schame.prisma
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個作成しました。

Terminal window
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" }]
})
}
Terminal window
$ 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 = 1
ORDER BY
`example`.`posts`.`id` ASC
LIMIT
? 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" }]
})
}
Terminal window
$ 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` ASC
LIMIT
? OFFSET ?

sqlformatterを使って整形しています。

前のページを取得する

現在3ページ目にいると仮定します。前のページのデータ(id: 5 ~ 10)はどうやって取得したら良いでしょうか?

async function findMany() {
return await prisma.posts.findMany({
take: 5,
skip: 1,
cursor: {
id: 10
},
orderBy: [{ id: "asc" }]
})
}
Terminal window
$ 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ページ目と同じデータを取得することができました。

Terminal window
$ 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` DESC
LIMIT
? OFFSET ?

sqlformatterを使って整形しています。

図で表現すると以下のようなイメージです。 prev-cursor

Previous Page Token for Cursor Paginationにも同じテクニックが書いてありました。

まとめ

prismaでcusor baseのページネーションをするとき以下のことがわかりました

  • 次のページに進みたいときはcursorに値を設定し、takeとskipを設定する。
  • 前のページに戻りたいときはcursorに値を設定し、takeをマイナスにして設定する。

今回使用したコードは blog-code/prisma-cursor-based-pagination - GitHubにあります。

参考