レビュー済み·難易度: 中級·更新: 2026-04-18

Prisma でフィルタ条件を動的に組み立ててクエリを実行する

URL クエリパラメータから受け取った検索条件を Prisma の where 句に動的に組み立て、キーワード・カテゴリ・難易度などの複合フィルタクエリを実行するパターン。

nextjsapisearch-filterprisma

対応バージョン

nextjs 15react 19prisma 5

前提環境

Prisma の基本的な CRUD 操作と Next.js Server Component のデータフェッチを理解していること

概要

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 を渡したプロパティはクエリ条件から除外される。nullIS NULL として解釈されるため、「条件なし」には必ず undefined を使う
  • contains + mode: "insensitive" で大文字小文字を無視した部分一致検索ができる。PostgreSQL では ILIKE にコンパイルされる
  • キーワード検索を OR で複数フィールドに広げる場合、スプレッド演算子 ...{} で条件ブロックを動的に追加する。条件なし時は空オブジェクトをスプレッドして何も追加しない
  • countfindManyPromise.all で並行実行することで、ページネーションに必要な総件数と現ページのデータを 1 往復で取得できる
  • filter.q || undefined の変換は、空文字 ("") を undefined に変えるために必要。空文字をそのまま渡すと contains: "" となり全件マッチしてしまう
  • mode: "insensitive" は MySQL では非対応(常に case-insensitive)なため、DB エンジンによって挙動が変わる点に注意

注意点

nextjs-url-filter-reset は URL クエリのリセット操作。jest-url-search-params-test はクエリ変換ロジックのテスト。これは Prisma where 句への動的条件組み立てと undefined を利用した条件省略パターンに特化。

関連サンプル