概要
URL クエリパラメータから受け取った検索条件(キーワード・カテゴリ・難易度)を Prisma の where 句に動的に組み立てる。undefined を渡すと Prisma がその条件を無視するプロパティを活用し、条件の有無に関係なく同一のクエリ関数で対応できるパターンを示す。
インストール
npm install prisma @prisma/client
npx prisma init
実装
Prisma スキーマ(参考)
// prisma/schema.prisma
model Sample {
id String @id @default(cuid())
slug String @unique
title String
summary String
category String
difficulty String
updatedAt DateTime @updatedAt
}
where 句を動的に組み立てる関数
// lib/db/samples.ts
import { prisma } from "@/lib/prisma";
type FilterParams = {
q?: string;
category?: string;
difficulty?: string;
};
export async function findSamples(filter: FilterParams) {
return prisma.sample.findMany({
where: {
// q が undefined のときこの条件全体が無視される
...(filter.q
? {
OR: [
{ title: { contains: filter.q, mode: "insensitive" } },
{ summary: { contains: filter.q, mode: "insensitive" } },
],
}
: {}),
// undefined を渡すと Prisma はその条件をスキップする
category: filter.category ?? undefined,
difficulty: filter.difficulty ?? undefined,
},
orderBy: { updatedAt: "desc" },
});
}
Server Component から呼び出す
// app/samples/page.tsx(抜粋)
import { findSamples } from "@/lib/db/samples";
type SearchParams = {
q?: string;
category?: string;
difficulty?: string;
};
type Props = {
searchParams: Promise<SearchParams>;
};
export default async function SamplesPage({ searchParams }: Props) {
const params = await searchParams;
const samples = await findSamples({
q: params.q || undefined,
category: params.category || undefined,
difficulty: params.difficulty || undefined,
});
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{samples.map((sample) => (
<div key={sample.id}>{sample.title}</div>
))}
</div>
);
}
ページネーションと組み合わせる
// lib/db/samples.ts(ページネーション版)
const PAGE_SIZE = 24;
export async function findSamplesPaged(filter: FilterParams, page = 1) {
const where = {
...(filter.q
? {
OR: [
{ title: { contains: filter.q, mode: "insensitive" as const } },
{ summary: { contains: filter.q, mode: "insensitive" as const } },
],
}
: {}),
category: filter.category ?? undefined,
difficulty: filter.difficulty ?? undefined,
};
const [total, items] = await Promise.all([
prisma.sample.count({ where }),
prisma.sample.findMany({
where,
orderBy: { updatedAt: "desc" },
skip: (page - 1) * PAGE_SIZE,
take: PAGE_SIZE,
}),
]);
return {
items,
total,
totalPages: Math.ceil(total / PAGE_SIZE),
};
}
ポイント
- Prisma では
undefinedを渡したプロパティはクエリ条件から除外される。nullはIS NULLとして解釈されるため、「条件なし」には必ずundefinedを使う contains+mode: "insensitive"で大文字小文字を無視した部分一致検索ができる。PostgreSQL ではILIKEにコンパイルされる- キーワード検索を
ORで複数フィールドに広げる場合、スプレッド演算子...{}で条件ブロックを動的に追加する。条件なし時は空オブジェクトをスプレッドして何も追加しない countとfindManyをPromise.allで並行実行することで、ページネーションに必要な総件数と現ページのデータを 1 往復で取得できるfilter.q || undefinedの変換は、空文字 ("") をundefinedに変えるために必要。空文字をそのまま渡すとcontains: ""となり全件マッチしてしまうmode: "insensitive"は MySQL では非対応(常に case-insensitive)なため、DB エンジンによって挙動が変わる点に注意