👉

prismaでcursorと同等のことを自前する

2023/05/11に公開

Prismaはpaginationの機能としてskipを利用するOffset paginationとcusorを利用するCursor-based paginationがある。

今回のような検証をするために、PrismaClientはlog出力設定をしておく

const prisma = new PrismaClient({
  log: ['query']
})

Cursor-basedを利用する場合、下記のように指定する。

const cursorId = 10
const tasks = await prisma.task.findMany({
  orderBy: {
    id: "desc"
  },
  cursor: {
    id: cursorId
  }
})

ドキュメントにある通り、基本的に、idはソート可能な事が前提として作られている

この場合、下記のようなクエリが生成される

SELECT
  `main`.`Task`.`id`,
  `main`.`Task`.`name`,
  `main`.`Task`.`createdAt`
FROM
  `main`.`Task`
WHERE
  `main`.`Task`.`id` <= (
    SELECT
      `main`.`Task`.`id`
    FROM
      `main`.`Task`
    WHERE
      (`main`.`Task`.`id`) = (?)
  )
ORDER BY
  `main`.`Task`.`id` DESC
LIMIT ? OFFSET ?

WHEREの中でサブクエリとして発行している。
IDがソート可能な前提とはいえ、なかなか好みの分かれるクエリが生成される。

また、下記のようにcreatedAtなどcursorに利用される以外の値を利用したい場合には更に問題を起こすクエリが発行される。

const cursorId = "04c44c77-6491-4343-a832-c242647d1d51"

const tasks = await prisma.task.findMany({
  orderBy: {
    createdAt: "desc"
  },
  cursor: {
    id: cursorId
  }
})
SELECT
  `main`.`Task`.`id`,
  `main`.`Task`.`name`,
  `main`.`Task`.`createdAt`
FROM
  `main`.`Task`
WHERE
  `main`.`Task`.`createdAt` <= (
    SELECT
      `main`.`Task`.`createdAt`
    FROM
      `main`.`Task`
    WHERE
      (`main`.`Task`.`id`) = (?)
  )
ORDER BY
  `main`.`Task`.`createdAt` DESC
LIMIT
  ? OFFSET ?

Cursorを自前で作成する

やっていることはカーソルのデータを取得して、それを利用するように分解すれば良いので、クエリを分解する

const cursorId = "04c44c77-6491-4343-a832-c242647d1d51"
const cursorTask = await prisma.task.findUniqueOrThrow({
  where: {
    id: cursorId
  }
})

const tasks = await prisma.task.findMany({
  where: {
    createdAt: {
      lte: cursorTask.createdAt
    }
  },
  orderBy: {
    id: "desc"
  },
})
GitHubで編集を提案

Discussion