Prisma Client
schema.prisma から自動生成された型付き ORM Client。 find / create / update / delete から where 演算子、ページング、集計まで。
基本
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
// 各 model に対して prisma.<model 名(小文字)>.メソッド
prisma.user.findMany()
prisma.post.create({ data: { title: "..." } })
SELECT(読み取り)
findMany
const users = await prisma.user.findMany()
// 条件
const adults = await prisma.user.findMany({
where: { age: { gte: 18 } }
})
// 列を絞る
const list = await prisma.user.findMany({
select: { id: true, email: true }
})
// ソート + ページング
const page = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
skip: 20,
take: 10,
})
findUnique(主キー / unique 列)
const user = await prisma.user.findUnique({
where: { id: "abc" }
})
// or
const user2 = await prisma.user.findUnique({
where: { email: "alice@example.com" }
})
findFirst(最初の 1 件)
const post = await prisma.post.findFirst({
where: { authorId: userId },
orderBy: { createdAt: "desc" },
})
findUniqueOrThrow / findFirstOrThrow
// 見つからなければ例外
const user = await prisma.user.findUniqueOrThrow({
where: { id }
})
where 演算子
prisma.post.findMany({
where: {
// 等価
status: "published",
// 比較
views: { gt: 100, lte: 1000 },
// IN
category: { in: ["tech", "design"] },
category2: { notIn: ["draft"] },
// 文字列
title: { contains: "react", mode: "insensitive" },
title: { startsWith: "How" },
title: { endsWith: "?" },
// null
deletedAt: null,
deletedAt: { not: null },
// 配列
tags: { has: "javascript" },
tags: { hasSome: ["js", "ts"] },
tags: { hasEvery: ["js", "react"] },
// AND / OR / NOT
AND: [{ published: true }, { views: { gt: 100 } }],
OR: [{ authorId: me }, { collaborators: { some: { userId: me } } }],
NOT: [{ status: "draft" }],
}
})
INSERT(作成)
// 1 件
const user = await prisma.user.create({
data: { email: "alice@example.com", name: "Alice" }
})
// 関連も同時作成(ネスト)
const userWithPosts = await prisma.user.create({
data: {
email: "bob@example.com",
posts: {
create: [
{ title: "first post" },
{ title: "second post" },
]
}
},
include: { posts: true }
})
// 複数件
await prisma.post.createMany({
data: [
{ title: "a", authorId },
{ title: "b", authorId },
],
skipDuplicates: true, // 衝突は無視
})
// createManyAndReturn(v5+)
const posts = await prisma.post.createManyAndReturn({
data: [...]
})
UPDATE
// 1 件(unique で)
await prisma.user.update({
where: { id },
data: { name: "Alice 2" }
})
// 複数件
await prisma.post.updateMany({
where: { authorId },
data: { published: true }
})
// 数値演算
await prisma.post.update({
where: { id },
data: { views: { increment: 1 } }
// decrement / multiply / divide / set
})
UPSERT
await prisma.user.upsert({
where: { email },
update: { lastLogin: new Date() },
create: { email, name: "(unknown)" }
})
DELETE
await prisma.post.delete({ where: { id } })
await prisma.post.deleteMany({ where: { authorId } })
select と include
select: 必要な列だけ取得
const post = await prisma.post.findUnique({
where: { id },
select: {
id: true,
title: true,
author: { select: { id: true, name: true } }
}
})
// 戻り値の型: { id, title, author: { id, name } }
include: 関連テーブルも一緒に
const post = await prisma.post.findUnique({
where: { id },
include: { author: true, comments: true }
})
select と include は同時に使えない(select の中で関連は select する)。
ページング
skip / take(オフセット式)
await prisma.post.findMany({
skip: page * pageSize,
take: pageSize,
orderBy: { createdAt: "desc" },
})
cursor(カーソル式 / 大量データ向け)
await prisma.post.findMany({
take: 10,
skip: 1, // cursor 自体を含めない
cursor: { id: lastId },
orderBy: { id: "asc" },
})
オフセット式は遅くなる(後ろのページほど)。大量データではカーソル式推奨。
count
const total = await prisma.post.count({ where: { published: true } })
// 関連数
const userWithCount = await prisma.user.findUnique({
where: { id },
include: { _count: { select: { posts: true } } }
})
console.log(userWithCount?._count.posts)
aggregate
const stats = await prisma.post.aggregate({
where: { published: true },
_count: { _all: true },
_avg: { views: true },
_max: { views: true },
_sum: { views: true },
})
groupBy
const byCategory = await prisma.post.groupBy({
by: ["category"],
where: { published: true },
_count: { _all: true },
_avg: { views: true },
having: { _count: { _all: { gt: 5 } } },
orderBy: { _count: { _all: "desc" } },
})
distinct
const cats = await prisma.post.findMany({
distinct: ["category"],
select: { category: true },
})
型推論
戻り値の型を取り出す
import { Prisma } from "@prisma/client"
type PostWithAuthor = Prisma.PostGetPayload<{
include: { author: true }
}>
function render(post: PostWithAuthor) {
console.log(post.author.name)
}
引数の型
const args = {
where: { published: true },
include: { author: true },
} satisfies Prisma.PostFindManyArgs
JSON 列の操作
await prisma.post.update({
where: { id },
data: {
meta: {
tags: ["js", "react"],
views: 100,
}
}
})
// JSON フィルタ
await prisma.post.findMany({
where: {
meta: {
path: ["tags"],
array_contains: ["js"]
}
}
})
NULL 処理
- field: null — その列が NULL の行を検索
- field: undefined — その列を条件から外す(重要)
// 「カテゴリが指定されていれば絞る」のパターン
prisma.post.findMany({
where: {
category: filter.category ?? undefined, // undefined なら無視
}
})
$queryRaw(生 SQL)
// テンプレートリテラル形式(自動エスケープ)
const result = await prisma.$queryRaw`
SELECT id, title FROM "Post"
WHERE created_at > ${oneWeekAgo}
`
// 型を指定
type Row = { id: string, title: string }
const rows = await prisma.$queryRaw<Row[]>`SELECT ...`
$executeRaw(INSERT/UPDATE/DELETE)
await prisma.$executeRaw`UPDATE "Post" SET views = views + 1 WHERE id = ${id}`
$disconnect
await prisma.$disconnect()
スクリプト終了時に呼ぶ。長期プロセス(サーバ)は基本不要。
エラーハンドリング
import { Prisma } from "@prisma/client"
try {
await prisma.user.create({ data })
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
// unique 制約違反
}
}
throw e
}
主要エラーコード
- P2000: 値が型に収まらない
- P2002: unique 制約違反
- P2003: FK 制約違反
- P2025: 操作対象が見つからない
Prisma Client のクエリログ
const prisma = new PrismaClient({ log: ["query", "warn", "error"] })
Optional vs Nullable
- schema で
name String?なら、TS ではname: string | null - create 時の
dataでは undefined を渡すと「省略」、null を渡すと NULL に
連鎖ナビゲーション(Fluent API)
const posts = await prisma.user.findUnique({ where: { id } }).posts()
親の主キーから関連を辿る糖衣構文。チェーンの途中の null は undefined。
パフォーマンス Tips
- select で列を絞る(特に大きい text / json)
- include はネスト深いと N+1 風に重くなることがある
- 大量データはcursor でページング
- 同時クエリは
Promise.allで並列 - 不要な ORM 化は
$queryRawに逃がす
失敗パターン
| 症状 | 対処 |
|---|---|
| P2002 重複 | upsert に変える / アプリで先に exist 確認 |
| P2025 not found | findUnique と組み合わせるか、try/catch |
| 遅い | EXPLAIN で確認、index 追加、select 絞る |
| BigInt の JSON 化失敗 | JSON.stringify の replacer で string 化 |
| Decimal の比較 | Decimal.js のメソッドを使う |
最初に覚える 5 つ
findMany / findUnique / create / update / delete。
これと where 演算子だけで 80% は片付く。残りは include / select。