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

Prisma でリレーションを含むクエリを書く(include / select / ネスト)

Prisma の include / select を使った 1対多・多対多のリレーション取得パターンと、ネストしたフィルタ・ソート・集計(_count)の例。

nextjscrudprisma

対応バージョン

nextjs 15react 19prisma 5

前提環境

Prisma の基本 CRUD(prisma-crud-api)とスキーマのリレーション定義を理解していること

概要

Prisma の include / select を使ったリレーションクエリパターン。1対多(User → Post)と多対多(Post ↔ Tag)の取得・ネストフィルタ・ソート・_count 集計を示す。

インストール

npm install prisma @prisma/client
npx prisma init

実装

スキーマ定義(1対多 + 多対多)

// prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String @unique
  posts Post[] // 1対多: User → Post
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  published   Boolean   @default(false)
  authorId    Int
  author      User      @relation(fields: [authorId], references: [id])
  tags        Tag[]     // 多対多: Post ↔ Tag(暗黙の中間テーブル)
  createdAt   DateTime  @default(now())
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[] // 多対多の逆側
}

Prisma クライアント(シングルトン)

// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ?? new PrismaClient({ log: ["query"] });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

include: 関連データをまとめて取得

// lib/queries.ts
import { prisma } from "@/lib/prisma";

// 1対多: ユーザーとその投稿を一括取得
export async function getUserWithPosts(userId: number) {
  return prisma.user.findUnique({
    where: { id: userId },
    include: {
      posts: {
        where: { published: true },        // 公開済みのみ
        orderBy: { createdAt: "desc" },    // 新しい順
        take: 10,                          // 最大10件
      },
    },
  });
}

// 多対多: 投稿とタグを一括取得
export async function getPostWithTags(postId: number) {
  return prisma.post.findUnique({
    where: { id: postId },
    include: {
      author: true,  // 著者情報も含める
      tags: true,    // 全タグを含める
    },
  });
}

select: 必要なフィールドだけ取得

// lib/queries.ts(続き)

// select: id / title / author.name だけを取得(over-fetching を避ける)
export async function getPostSummaries() {
  return prisma.post.findMany({
    where: { published: true },
    select: {
      id: true,
      title: true,
      createdAt: true,
      author: {
        select: { name: true }, // 著者名だけ
      },
      tags: {
        select: { name: true }, // タグ名だけ
      },
    },
    orderBy: { createdAt: "desc" },
  });
}

_count: 集計カウントをクエリに含める

// lib/queries.ts(続き)

// 投稿数を含むユーザー一覧
export async function getUsersWithPostCount() {
  return prisma.user.findMany({
    include: {
      _count: {
        select: { posts: true }, // postsの件数
      },
    },
    orderBy: {
      posts: { _count: "desc" }, // 投稿数降順
    },
  });
}

// タグ付き投稿の件数でソート
export async function getTagsWithPostCount() {
  return prisma.tag.findMany({
    include: {
      _count: {
        select: { posts: true },
      },
    },
    orderBy: {
      posts: { _count: "desc" },
    },
  });
}

ネストフィルタ: 条件を関連モデルにかける

// lib/queries.ts(続き)

// 特定タグを持つ投稿を取得
export async function getPostsByTag(tagName: string) {
  return prisma.post.findMany({
    where: {
      published: true,
      tags: {
        some: { name: tagName }, // 少なくとも1つのタグが一致
      },
    },
    include: { author: true, tags: true },
    orderBy: { createdAt: "desc" },
  });
}

// 投稿が1件以上あるユーザーのみ取得
export async function getActiveUsers() {
  return prisma.user.findMany({
    where: {
      posts: {
        some: { published: true }, // 公開済み投稿が1件以上
      },
    },
    include: {
      _count: { select: { posts: true } },
    },
  });
}

Route Handler での使用例

// app/api/users/[id]/posts/route.ts
import { NextResponse } from "next/server";
import { getUserWithPosts } from "@/lib/queries";

type Params = { params: Promise<{ id: string }> };

export async function GET(_req: Request, { params }: Params) {
  const { id } = await params;
  const user = await getUserWithPosts(Number(id));

  if (!user) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  return NextResponse.json(user);
}

ポイント

  • include は関連モデルのすべてのフィールドを取得する。select は必要なフィールドだけを指定する。大量データを扱う場合は select で over-fetching を防ぐ
  • includeselect は同一クエリで同時に使えない。どちらかを選ぶ(関連モデルのフィールドを絞りたいなら select 内でネストして指定する)
  • ネストフィルタの演算子: some(1件以上一致)/ every(全件一致)/ none(0件一致)。関連モデルを条件にした絞り込みで使う
  • _count で関連モデルの件数を集計できる。orderBy にも使えるため「投稿数が多い順」などのソートが 1 クエリで完結する
  • 多対多の暗黙の中間テーブルは Prisma が自動管理する。明示的な中間テーブルが必要な場合(追加フィールドを持つ場合)は @relation でモデルを定義する

注意点

prisma-crud-api は単一モデルの基本 CRUD。prisma-soft-delete は論理削除パターン。これは include / select によるリレーション取得・ネストフィルタ・_count 集計に特化。1対多(User → Post)と多対多(Post ↔ Tag)の両方のパターンを示す。

関連サンプル