Prisma 大量データの取得クエリを Cursor ベースのページネーションで改善する
はじめに
最近は Next.js がフルスタックアプリケーションフレームワークとして成熟してきたこともあり、Prisma を ORM として採用しています。
Prisma は直感的な API を提供する一方で、特定の使用パターンでパフォーマンスの問題を引き起こすことがあります。
この記事では、Prisma を使用した大量データの取得方法の改善方法について Cursor ベースのページネーションの実装について紹介します。
簡単な実装例
// ❌ 良くない例:全データの一括取得
const users = await prisma.user.findMany({
skip: 100, // 最初の 100 件をスキップ
take: 10 // 次の 10 件を取得
});
// ✅ 最適化例:カーソルベースのページネーション
const users = await prisma.user.findMany({
cursor: {
id: 100 // ID が 100 より大きいレコードから
},
take: 10 // 10 件を取得
});
なぜ cursor が推奨されるのか
1. パフォーマンス
offset は指定した数のレコードを読み飛ばす必要がある
cursor は特定の位置から直接読み取りを開始できる
// 例:1000ページ目を取得する場合
// ❌ offset の場合:最初の 10000 レコードを読み込んでから破棄
await prisma.user.findMany({
skip: 10000,
take: 10
});
// ✅ cursor の場合:ID 10000 以降のレコードから直接読み取り
await prisma.user.findMany({
cursor: { id: 10000 },
take: 10
});
2. データの一貫性
offset の場合、データが追加/削除されると結果が変わる可能性がある
cursor は固定の基準点を使用するため、より一貫性がある
// app/api/users/route.ts
import { prisma } from '@/lib/prisma';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
// searchParams から cursor と pageSize を取得
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor');
const pageSize = 10;
const users = await prisma.user.findMany({
// cursor が存在する場合、その ID 以降から取得
...(cursor
? {
cursor: {
id: parseInt(cursor),
},
skip: 1, // cursor として使用したレコードをスキップ
}
: {}),
take: pageSize + 1, // 次ページの有無を確認するため 1 件多く取得
orderBy: {
id: 'asc',
},
// 必要なフィールドのみを取得
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
});
// 次ページの有無を確認
const hasNextPage = users.length > pageSize;
// 実際に返すデータは pageSize 分まで
const data = users.slice(0, pageSize);
// 次の cursor は最後のレコードの ID
const nextCursor = hasNextPage ? data[data.length - 1].id : null;
return NextResponse.json({
users: data,
pagination: {
nextCursor,
hasNextPage,
},
});
} catch (error) {
console.error('Failed to fetch users:', error);
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
// 検索条件付きの場合
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor');
const pageSize = 10;
const users = await prisma.user.findMany({
where: {
// 検索条件を追加
name: {
contains: body.searchTerm,
},
status: body.status,
// 他の条件も追加可能
},
...(cursor
? {
cursor: {
id: parseInt(cursor),
},
skip: 1,
}
: {}),
take: pageSize + 1,
orderBy: {
id: 'asc',
},
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
});
const hasNextPage = users.length > pageSize;
const data = users.slice(0, pageSize);
const nextCursor = hasNextPage ? data[data.length - 1].id : null;
return NextResponse.json({
users: data,
pagination: {
nextCursor,
hasNextPage,
},
});
} catch (error) {
console.error('Failed to search users:', error);
return NextResponse.json(
{ error: 'Failed to search users' },
{ status: 500 }
);
}
}