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