Prisma は TypeScript との親和性が抜群に高いORMです。schema.prisma にテーブル定義を書くだけで、型安全なクライアントコードが自動生成され、クエリの引数・戻り値・リレーションのデータすべてに正確な型が付きます。
この記事では Prisma が生成する型の構造を理解した上で、CRUD・リレーション・トランザクション・エラー処理まで、実務で即使えるパターンを TypeScript の型定義を中心に解説します。
- Prisma が
schema.prismaから自動生成する型の種類と使い方 - 型安全な CRUD 操作(findUnique・findMany・create・update・delete)
include/selectによるリレーションデータの型推論- 型安全なフィルタリング・ソート・ページネーション
- トランザクション(
$transaction)の型定義 - Prisma エラー(
PrismaClientKnownRequestError)の型安全な処理 - PrismaClient 拡張(Extensions)と型定義
- Next.js / Express でのシングルトンパターン
セットアップ
# Prisma のインストール npm install prisma --save-dev npm install @prisma/client # Prisma の初期化(schema.prisma が生成される) npx prisma init # または SQLite でローカル開発 npx prisma init --datasource-provider sqlite
// .env DATABASE_URL="postgresql://user:password@localhost:5432/mydb" // SQLite の場合 // DATABASE_URL="file:./dev.db"
PrismaClient のシングルトン初期化
Next.js 等のホットリロード環境では PrismaClient が複数インスタンス生成されることがあります。シングルトンパターンで防ぎましょう。
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
global(Node.js固有)ではなく globalThis(ECMAScript標準)を使うことで、TypeScript の型チェックが通り、Edge Runtime(Vercel Edge / Cloudflare Workers)でも動作します。as unknown as { prisma: ... } のダブルアサーションはglobalThis の型定義に prisma が含まれていないための必要な回避策です。スキーマ定義と自動生成型
schema.prisma の定義例
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // ソフトデリート用
posts Post[]
profile Profile?
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
tags Tag[] @relation("PostTags")
createdAt DateTime @default(now())
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[] @relation("PostTags")
}
enum Role {
USER
ADMIN
MODERATOR
}
# スキーマを変更したら必ず実行 npx prisma generate # TypeScript 型を再生成 npx prisma migrate dev # DBにマイグレーションを適用 npx prisma db push # マイグレーションなしで同期(プロトタイプ向け)
自動生成される型の種類
npx prisma generate を実行すると @prisma/client に以下の型が生成されます。
| 型名 | 用途 | 例 |
|---|---|---|
User |
モデルのデータ型(全フィールド) | { id: number; email: string; name: string | null; ... } |
Prisma.UserCreateInput |
作成時の入力型 | { email: string; name?: string; role?: Role } |
Prisma.UserUpdateInput |
更新時の入力型(全フィールドオプション) | { email?: string; name?: string | null; ... } |
Prisma.UserWhereInput |
フィルタ条件型 | { email?: StringFilter; role?: Role; ... } |
Prisma.UserWhereUniqueInput |
ユニーク条件型 | { id?: number; email?: string } |
Prisma.UserOrderByWithRelationInput |
ソート条件型 | { createdAt?: SortOrder } |
Prisma.UserSelect |
取得フィールド指定型 | { id?: boolean; email?: boolean; ... } |
Prisma.UserInclude |
リレーション読み込み型 | { posts?: boolean | PostFindManyArgs; ... } |
Prisma.UserGetPayload<T> |
select/include を反映した戻り値型 | Prisma.UserGetPayload<{ include: { posts: true } }> |
Role |
enum 型 | "USER" | "ADMIN" | "MODERATOR" |
CRUD 操作と型定義
Read:findUnique / findFirst / findMany
import { prisma } from "@/lib/prisma";
import type { User } from "@prisma/client";
// ─── findUnique ─── 戻り値: User | null
async function getUserById(id: number): Promise<User | null> {
return prisma.user.findUnique({
where: { id }, // Prisma.UserWhereUniqueInput
});
}
// ─── findFirstOrThrow ─── 見つからない場合は PrismaClientKnownRequestError
async function getUserByEmail(email: string): Promise<User> {
return prisma.user.findFirstOrThrow({
where: { email },
});
}
// ─── findMany ─── 戻り値: User[]
async function getAdminUsers(): Promise<User[]> {
return prisma.user.findMany({
where: { role: "ADMIN" },
orderBy: { createdAt: "desc" },
take: 20,
skip: 0,
});
}
// ─── count ─── 戻り値: number
async function countUsers(): Promise<number> {
return prisma.user.count({
where: { role: "USER" },
});
}
Create:create / createMany
import type { Prisma, User } from "@prisma/client";
// Prisma.UserCreateInput を使って入力型を明示
async function createUser(data: Prisma.UserCreateInput): Promise<User> {
return prisma.user.create({ data });
}
// 利用例
const newUser = await createUser({
email: "alice@example.com",
name: "Alice",
role: "USER", // Role enum の値
// posts は undefined でも OK(optional)
// createdAt は @default(now()) なので不要
});
// newUser の型: User(全フィールドが含まれる)
// ─── createMany ─── 戻り値: Prisma.BatchPayload({ count: number })
async function seedUsers(): Promise<Prisma.BatchPayload> {
return prisma.user.createMany({
data: [
{ email: "bob@example.com", name: "Bob" },
{ email: "carol@example.com", name: "Carol", role: "ADMIN" },
],
skipDuplicates: true,
});
}
Update:update / upsert / updateMany
import type { Prisma, User } from "@prisma/client";
// ─── update ─── where + data が必須
async function updateUserName(
id: number,
name: string
): Promise<User> {
return prisma.user.update({
where: { id },
data: { name }, // Prisma.UserUpdateInput
});
}
// ─── upsert ─── 存在すれば update、なければ create
async function upsertUser(
email: string,
name: string
): Promise<User> {
return prisma.user.upsert({
where: { email }, // UserWhereUniqueInput
update: { name }, // UserUpdateInput
create: { email, name }, // UserCreateInput
});
}
// ─── updateMany ─── 複数件更新、戻り値は BatchPayload
async function deactivateOldUsers(before: Date): Promise<Prisma.BatchPayload> {
return prisma.user.updateMany({
where: { createdAt: { lt: before } },
data: { role: "USER" },
});
}
Delete:delete / deleteMany
// ─── delete ─── 戻り値: User(削除されたレコード)
async function deleteUser(id: number): Promise<User> {
return prisma.user.delete({
where: { id },
});
}
// ─── deleteMany ─── 戻り値: Prisma.BatchPayload
async function deleteUnverifiedUsers(): Promise<Prisma.BatchPayload> {
return prisma.user.deleteMany({
where: { email: { endsWith: "@temp.com" } },
});
}
リレーションデータの型推論(include / select)
Prisma の最大の型安全機能が include / select の型推論です。どのフィールドを取得するかに応じて、戻り値の型が自動的に変化します。
include で関連データを取得する
import type { Prisma } from "@prisma/client";
// include: { posts: true } で User + Post[] が戻り値型に含まれる
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
});
// userWithPosts の型:
// (User & { posts: Post[] }) | null
// userWithPosts?.posts は Post[] 型
// ネストした include(Post の author も含める)
const postWithAll = await prisma.post.findUnique({
where: { id: 1 },
include: {
author: true, // User 型
tags: true, // Tag[] 型
},
});
// postWithAll?.author は User 型
// postWithAll?.tags は Tag[] 型
// ─── Prisma.UserGetPayload で型を事前定義 ───
// 戻り値型を関数の引数・戻り値として共有したい場合
type UserWithPosts = Prisma.UserGetPayload<{
include: { posts: true };
}>;
function displayUser(user: UserWithPosts): string {
// user.posts は Post[] 型として使える
return `${user.name}: ${user.posts.length}件の投稿`;
}
select で取得フィールドを絞る
// select で取得するフィールドを指定すると、型が自動的に絞られる
const userSummary = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
// createdAt・updatedAt・role は取得しない
},
});
// userSummary の型: { id: number; email: string; name: string | null }[]
// role や createdAt にアクセスしようとするとコンパイルエラー
// select とリレーションの組み合わせ
const usersWithPostCount = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: { select: { posts: true } }, // 集計フィールド
posts: {
select: {
id: true,
title: true,
// content は除外
},
where: { published: true }, // リレーション内フィルタも可能
},
},
});
// usersWithPostCount の型:
// { id: number; name: string | null; _count: { posts: number };
// posts: { id: number; title: string }[] }[]
// ─── Prisma.UserGetPayload で型を定義 ───
type UserSummary = Prisma.UserGetPayload<{
select: {
id: true;
email: true;
name: true;
posts: { select: { id: true; title: true } };
};
}>;
include と select を同一クエリで併用するとエラーになります。リレーションを含む特定フィールドだけを取得したい場合は select の中にネストした select を使ってください。(上記コード例の posts: { select: { ... } } のパターン)型安全なフィルタリング・ソート・ページネーション
import type { Prisma } from "@prisma/client";
// ─── 検索条件を型安全に組み立てる ───
interface PostFilterParams {
keyword?: string;
authorId?: number;
published?: boolean;
tags?: string[];
page?: number;
pageSize?: number;
}
async function findPosts(params: PostFilterParams) {
const { keyword, authorId, published, tags, page = 1, pageSize = 20 } = params;
// Prisma.PostWhereInput 型で where 条件を組み立てる
const where: Prisma.PostWhereInput = {
...(published !== undefined && { published }),
...(authorId !== undefined && { authorId }),
...(keyword && {
OR: [
{ title: { contains: keyword, mode: "insensitive" } },
{ content: { contains: keyword, mode: "insensitive" } },
],
}),
...(tags?.length && {
tags: { some: { name: { in: tags } } },
}),
};
const [posts, total] = await prisma.$transaction([
prisma.post.findMany({
where,
orderBy: { createdAt: "desc" },
take: pageSize,
skip: (page - 1) * pageSize,
include: { author: { select: { name: true, email: true } } },
}),
prisma.post.count({ where }),
]);
return {
posts,
total,
totalPages: Math.ceil(total / pageSize),
page,
};
}
equals・not:完全一致・不一致in・notIn:配列の中に含まれる・含まれないlt・lte・gt・gte:数値・日付の大小比較contains・startsWith・endsWith:文字列検索mode: "insensitive":大文字小文字を無視した検索(PostgreSQL)AND・OR・NOT:論理演算子(配列で複数条件)some・every・none:リレーション配列に対する条件
トランザクション
Sequential Transactions(配列スタイル)
// 複数クエリをまとめてトランザクション実行
// すべて成功すれば commit、一つでも失敗すれば rollback
// ※ User モデルに points: Int @default(0) フィールドがある前提
async function transferPoints(
fromUserId: number,
toUserId: number,
points: number
): Promise<void> {
// $transaction の引数は配列: PrismaPromise<T>[] を渡す
const [deductResult, addResult] = await prisma.$transaction([
prisma.user.update({
where: { id: fromUserId },
data: { points: { decrement: points } },
}),
prisma.user.update({
where: { id: toUserId },
data: { points: { increment: points } },
}),
]);
// deductResult・addResult は User 型
}
Interactive Transactions(関数スタイル)
// コールバックスタイル: 途中で条件分岐・throw が可能
async function createPostWithNotification(
authorId: number,
data: Prisma.PostCreateInput
): Promise<Post> {
return prisma.$transaction(async (tx) => {
// tx は PrismaClient と同じ API を持つ
const author = await tx.user.findUnique({ where: { id: authorId } });
if (!author) throw new Error("作成者が見つかりません");
const post = await tx.post.create({
data: { ...data, authorId },
});
await tx.notification.create({
data: {
userId: authorId,
message: `投稿「${post.title}」を作成しました`,
},
});
return post; // Promise が resolve されれば commit
});
// throw されれば自動的に rollback
}
Interactive Transaction はデフォルト 5 秒でタイムアウトします。重い処理や複数クエリを実行する場合は
{ timeout: 10000 }(ms)をオプションで指定してください:prisma.$transaction(async (tx) => { ... }, { timeout: 10000 })。また、トランザクション内ではシングルトンの prisma を使わず、引数の tx のみを使うこと(デッドロック防止)。Prisma エラーの型安全な処理
Prisma は独自のエラークラスを提供しています。これらを型で絞り込むことで、エラーコードに応じた処理を型安全に書けます。
import { Prisma } from "@prisma/client";
// ─── エラークラスの種類 ───
// Prisma.PrismaClientKnownRequestError → DB制約違反・レコード未検出など
// Prisma.PrismaClientUnknownRequestError → 不明なDBエラー
// Prisma.PrismaClientRustPanicError → エンジン内部エラー(まれ)
// Prisma.PrismaClientInitializationError → 接続失敗・環境変数未設定
// Prisma.PrismaClientValidationError → クエリ引数の型エラー
async function createUserSafe(
data: Prisma.UserCreateInput
): Promise<{ ok: true; user: User } | { ok: false; error: string }> {
try {
const user = await prisma.user.create({ data });
return { ok: true, user };
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
// error.code はエラーコード文字列(例: "P2002")
switch (e.code) {
case "P2002": // Unique constraint failed
// e.meta?.target は制約対象のフィールド名の配列
const fields = (e.meta?.target as string[])?.join(", ") ?? "不明";
return { ok: false, error: `${fields} はすでに使用されています` };
case "P2025": // Record not found
return { ok: false, error: "レコードが見つかりません" };
case "P2003": // Foreign key constraint failed
return { ok: false, error: "関連するレコードが存在しません" };
default:
return { ok: false, error: `DBエラー (${e.code}): ${e.message}` };
}
}
if (e instanceof Prisma.PrismaClientValidationError) {
return { ok: false, error: "クエリの引数が不正です" };
}
throw e; // 不明なエラーは再 throw
}
}
| エラーコード | 意味 | 発生例 |
|---|---|---|
P2002 |
Unique constraint 違反 | 同じ email が既に存在する |
P2003 |
Foreign key 制約違反 | 存在しない authorId を指定 |
P2025 |
レコードが見つからない | findUniqueOrThrow・update・delete でレコードが存在しない |
P2016 |
クエリ実行エラー | 型の不一致など |
P1001 |
DB接続失敗 | DATABASE_URL が誤っている |
P1002 |
DBタイムアウト | ネットワーク遅延・DB負荷 |
エラーハンドリングの設計パターンはTypeScript エラーハンドリング完全ガイドも参照してください。
Raw クエリの型安全な実行
import { Prisma } from "@prisma/client";
// ─── $queryRaw ─── 型引数で戻り値型を指定
interface UserStats {
role: string;
count: bigint; // PostgreSQL の COUNT() は bigint
}
async function getUserStats(): Promise<UserStats[]> {
return prisma.$queryRaw<UserStats[]>`
SELECT role, COUNT(*) as count
FROM "User"
GROUP BY role
ORDER BY count DESC
`;
}
// ─── Prisma.sql(テンプレートリテラル)で安全にパラメータを埋め込む ───
async function searchUsersByName(name: string): Promise<User[]> {
// Prisma.sql を使うと SQL インジェクションを防げる
return prisma.$queryRaw<User[]>(
Prisma.sql`SELECT * FROM "User" WHERE name ILIKE ${`%${name}%`}`
);
}
// ─── $executeRaw ─── 影響行数を返す(INSERT/UPDATE/DELETE)
async function archiveOldPosts(before: Date): Promise<number> {
// 戻り値は number(bigint ではない)
const result: number = await prisma.$executeRaw`
UPDATE "Post" SET archived = true
WHERE "createdAt" < ${before} AND published = false
`;
return result;
}
$queryRawUnsafe("SELECT * FROM User WHERE id = " + id) のように文字列連結でパラメータを埋め込むと SQL インジェクションが発生します。必ず Prisma.sql`...`(タグ付きテンプレート)または $queryRaw`...` を使い、パラメータはテンプレート変数として渡してください。PrismaClient 拡張(Extensions)
Prisma 4.7 以降で追加された Extensions を使うと、PrismaClient に独自のメソッドやミドルウェアを型安全に追加できます。
// lib/prisma.ts(Extensions 付き)
import { PrismaClient } from "@prisma/client";
const basePrisma = new PrismaClient();
// ─── $extends で独自メソッドを追加 ───
export const prisma = basePrisma.$extends({
model: {
user: {
// ソフトデリート(deleted_at を設定するだけで実際には削除しない)
async softDelete(id: number) {
return basePrisma.user.update({
where: { id },
data: { deletedAt: new Date() },
});
},
// 公開中のユーザー一覧を取得するショートカット
async findActive() {
return basePrisma.user.findMany({
where: { deletedAt: null },
});
},
},
},
// ─── クエリミドルウェア(パフォーマンスログ)───
query: {
async $allOperations({ operation, model, args, query }) {
const start = performance.now();
const result = await query(args);
const end = performance.now();
if (end - start > 1000) {
console.warn(`[Slow Query] ${model}.${operation}: ${Math.round(end - start)}ms`);
}
return result;
},
},
});
// 利用側では型補完が効く
// prisma.user.softDelete(1); // OK
// prisma.user.findActive(); // OK
// prisma.post.softDelete(1); // Error: post には softDelete が存在しない
Next.js / Express での実践パターン
リポジトリパターンで型を整理する
// repositories/userRepository.ts
import type { Prisma, User } from "@prisma/client";
import { prisma } from "@/lib/prisma";
// リポジトリが扱う型を一か所にまとめる
type UserWithPosts = Prisma.UserGetPayload<{
include: { posts: { select: { id: true; title: true; published: true } } };
}>;
type CreateUserParams = Pick<Prisma.UserCreateInput, "email" | "name">;
type UpdateUserParams = Partial<Pick<Prisma.UserUpdateInput, "name" | "role">>;
export const userRepository = {
// 一覧取得
async findAll(): Promise<User[]> {
return prisma.user.findMany({
where: { deletedAt: null },
orderBy: { createdAt: "desc" },
});
},
// ID で取得(投稿付き)
async findByIdWithPosts(id: number): Promise<UserWithPosts | null> {
return prisma.user.findUnique({
where: { id },
include: { posts: { select: { id: true, title: true, published: true } } },
});
},
// 作成
async create(params: CreateUserParams): Promise<User> {
return prisma.user.create({ data: params });
},
// 更新
async update(id: number, params: UpdateUserParams): Promise<User> {
return prisma.user.update({ where: { id }, data: params });
},
// 論理削除
async softDelete(id: number): Promise<User> {
return prisma.user.update({
where: { id },
data: { deletedAt: new Date() },
});
},
};
Next.js Route Handler での利用
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { userRepository } from "@/repositories/userRepository";
import { Prisma } from "@prisma/client";
import { z } from "zod";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).optional(),
});
export async function GET(): Promise<NextResponse> {
const users = await userRepository.findAll();
return NextResponse.json(users);
}
export async function POST(request: NextRequest): Promise<NextResponse> {
const parseResult = createUserSchema.safeParse(await request.json());
if (!parseResult.success) {
return NextResponse.json({ error: parseResult.error.flatten() }, { status: 400 });
}
try {
const user = await userRepository.create(parseResult.data);
return NextResponse.json(user, { status: 201 });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
return NextResponse.json({ error: "このメールアドレスは既に使用されています" }, { status: 409 });
}
throw e;
}
}
Zod によるリクエストバリデーションの詳細はTypeScript Zod 完全ガイドを、Route Handler の型定義はTypeScript Next.js ガイドを参照してください。
よくある質問
QPrismaClient の型(PrismaClient型)を関数の引数として受け取れますか?
Aできます。引数に PrismaClient 型を直接使うか、より厳密には Omit<PrismaClient, symbol> を使います。ただし Interactive Transaction のコールバックで受け取る tx はOmit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"> 型です。リポジトリ関数がトランザクション内で呼べるよう、引数型を柔軟に設計してください:function createPost(tx: Omit<PrismaClient, symbol>, data: ...)。
QPrisma.UserGetPayload と User 型の違いは?
AUser 型はモデルの全フィールドを含む基本型です(リレーション含まず)。Prisma.UserGetPayload<T> は select / include の指定を型引数に取り、クエリの戻り値に合わせた正確な型を計算します。リレーションを含むデータを関数間で受け渡す場合は UserGetPayload を使いましょう。
QPrisma と Zod を組み合わせて入力バリデーションを行う方法は?
Azod-prisma-types(コミュニティパッケージ)を使うと、schema.prisma から Zod スキーマを自動生成できます。手動で定義する場合は Prisma.UserCreateInput の型を参考に z.object({ email: z.string().email(), name: z.string().optional() }) のようなスキーマを作り、parse 後の値を Prisma クエリに渡してください。
QBigInt 型(COUNT の結果など)を JSON にシリアライズするにはどうすればよいですか?
AJSON.stringify はデフォルトで BigInt を処理できません。カスタムリプレイサーを使ってください:JSON.stringify(data, (_, v) => typeof v === "bigint" ? v.toString() : v)。または Number(bigIntValue) に変換する(9007199254740991 以下なら精度の問題なし)か、$queryRaw の型定義で count: number(bigint ではなく)とキャストして扱うのも実務上よく使われます。
Qソフトデリートを実装したとき、削除済みレコードを常に除外するにはどうすればよいですか?
APrisma の Middleware(旧)または Extensions の query フック(推奨)を使います。$extends({ query: { $allModels: { async findMany({ args, query }) { args.where = { ...args.where, deletedAt: null }; return query(args); } } } }) のようにwhere 条件を自動付与できます。ただしすべての findMany に適用されるため、管理画面など削除済みも表示したい箇所では別の PrismaClient インスタンスを使ってください。
まとめ
Prisma は schema.prisma を唯一の型の源泉(Single Source of Truth)として、すべてのクエリ操作に正確な型を自動生成します。
| 機能 | 型定義のポイント |
|---|---|
| CRUD 操作 | Prisma.UserCreateInput / UpdateInput / WhereInput を引数型に使う |
| リレーション | include: true で戻り値型に自動追加。UserGetPayload<T> で型を事前定義 |
| select | 取得フィールドに応じて戻り値型が自動的に絞られる |
| フィルタ | Prisma.PostWhereInput で型安全な where 条件を組み立て |
| トランザクション | 配列スタイル(並列 Promise)/ コールバックスタイル(条件分岐・throw 可能) |
| エラー処理 | instanceof Prisma.PrismaClientKnownRequestError でエラーコードを型安全に処理 |
| Extensions | $extends で独自メソッドを型補完付きで追加 |
Prisma の型システムを活用することで、DB とアプリケーション間のデータ型の不一致をコンパイル時に検出できます。TypeScript Express ガイド・TypeScript Next.js ガイド・ブランド型ガイドと組み合わせることで、フルスタックの型安全なアプリケーションを構築できます。

