Prisma高度パターン集 — 最適化・マイグレーション・テスト戦略完全ガイド
Prisma ORMは、TypeScriptでデータベース操作を型安全に行える最強のツールです。2026年現在、Next.js、Remix、NestJSなど主要フレームワークで標準的に使われています。
この記事では、Prismaの基礎を超えた高度なパターン、最適化手法、マイグレーション戦略、テスト手法を実践的に解説します。
Prisma基本セットアップ(復習)
インストールと初期化
npm install prisma @prisma/client
npx prisma init
スキーマ定義
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([published])
}
model Profile {
id String @id @default(cuid())
bio String?
avatar String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @unique
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
}
npx prisma migrate dev --name init
npx prisma generate
クエリ最適化パターン
N+1問題の解決
// ❌ N+1問題(遅い)
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({
where: { authorId: user.id },
});
console.log(user.name, posts.length);
}
// → 1 + N回のクエリ(ユーザーが100人なら101回のクエリ)
// ✅ includeで解決(速い)
const users = await prisma.user.findMany({
include: { posts: true },
});
users.forEach((user) => {
console.log(user.name, user.posts.length);
});
// → 1回のクエリのみ
selectで必要なフィールドのみ取得
// ❌ すべてのフィールドを取得(遅い)
const users = await prisma.user.findMany();
// ✅ 必要なフィールドのみ取得(速い)
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
ネストしたincludeの最適化
// 深くネストしたデータ取得
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
tags: {
select: {
id: true,
name: true,
},
},
},
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
});
カウントクエリの最適化
// ❌ すべてのデータを取得してカウント(遅い)
const users = await prisma.user.findMany();
const count = users.length;
// ✅ countを使う(速い)
const count = await prisma.user.count();
// 条件付きカウント
const publishedCount = await prisma.post.count({
where: { published: true },
});
// グループごとのカウント
const postCountByUser = await prisma.post.groupBy({
by: ['authorId'],
_count: {
id: true,
},
});
バッチクエリでパフォーマンス向上
// ❌ ループ内でクエリ(遅い)
const userIds = ['id1', 'id2', 'id3'];
for (const id of userIds) {
await prisma.user.update({
where: { id },
data: { updatedAt: new Date() },
});
}
// ✅ バッチ更新(速い)
await prisma.user.updateMany({
where: {
id: {
in: userIds,
},
},
data: { updatedAt: new Date() },
});
トランザクション
基本的なトランザクション
// $transactionで複数操作をアトミックに
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: 'user@example.com',
name: 'ユーザー',
},
});
await tx.profile.create({
data: {
userId: user.id,
bio: '自己紹介',
},
});
await tx.post.create({
data: {
title: '最初の投稿',
authorId: user.id,
},
});
});
// すべて成功するか、すべて失敗するか(ロールバック)
インタラクティブトランザクション
// 複雑な条件分岐を含むトランザクション
await prisma.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new Error('User not found');
}
if (user.credits < 100) {
throw new Error('Insufficient credits');
}
// クレジット減算
await tx.user.update({
where: { id: userId },
data: {
credits: {
decrement: 100,
},
},
});
// 購入記録作成
await tx.purchase.create({
data: {
userId: userId,
amount: 100,
},
});
});
分離レベルの指定
// 分離レベルを指定したトランザクション
await prisma.$transaction(
async (tx) => {
// トランザクション処理
},
{
isolationLevel: 'Serializable', // ReadUncommitted, ReadCommitted, RepeatableRead, Serializable
maxWait: 5000, // 5秒待機
timeout: 10000, // 10秒でタイムアウト
}
);
高度なスキーマパターン
複合主キー
model UserRole {
userId String
roleId String
user User @relation(fields: [userId], references: [id])
role Role @relation(fields: [roleId], references: [id])
@@id([userId, roleId])
}
JSONフィールド
model Settings {
id String @id @default(cuid())
userId String @unique
metadata Json @default("{}")
}
// JSON操作
await prisma.settings.create({
data: {
userId: 'user-id',
metadata: {
theme: 'dark',
language: 'ja',
notifications: {
email: true,
push: false,
},
},
},
});
// JSON内を検索
const settings = await prisma.settings.findMany({
where: {
metadata: {
path: ['notifications', 'email'],
equals: true,
},
},
});
全文検索
model Post {
id String @id @default(cuid())
title String
content String
@@fulltext([title, content])
}
// 全文検索
const posts = await prisma.post.findMany({
where: {
OR: [
{ title: { search: 'Prisma' } },
{ content: { search: 'Prisma' } },
],
},
});
ソフトデリート
model Post {
id String @id @default(cuid())
title String
deletedAt DateTime?
@@index([deletedAt])
}
// ソフトデリート
await prisma.post.update({
where: { id: postId },
data: { deletedAt: new Date() },
});
// 削除されていないものだけ取得
const posts = await prisma.post.findMany({
where: { deletedAt: null },
});
// ミドルウェアで自動フィルタリング
prisma.$use(async (params, next) => {
if (params.model === 'Post' && params.action === 'findMany') {
params.args.where = params.args.where || {};
params.args.where.deletedAt = null;
}
return next(params);
});
マイグレーション戦略
開発環境でのマイグレーション
# スキーマ変更後、マイグレーション作成
npx prisma migrate dev --name add_user_role
# マイグレーション履歴確認
npx prisma migrate status
# マイグレーションをロールバック
npx prisma migrate reset
本番環境でのマイグレーション
# マイグレーションを適用(データベースを変更)
npx prisma migrate deploy
# 本番環境でのCI/CD
# .github/workflows/deploy.yml
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
カスタムマイグレーション
-- prisma/migrations/20260206000000_custom/migration.sql
-- 複雑なデータ移行
UPDATE "User"
SET "email" = LOWER("email")
WHERE "email" != LOWER("email");
-- インデックス追加
CREATE INDEX CONCURRENTLY "User_email_idx" ON "User"("email");
-- 関数作成
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW."updatedAt" = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
ダウンタイムゼロのマイグレーション
// ステップ1: カラム追加(NULL許容)
model User {
id String @id
email String @unique
newEmail String? // まずはNULL許容で追加
}
npx prisma migrate dev --name add_new_email
// ステップ2: データ移行
await prisma.$executeRaw`
UPDATE "User"
SET "newEmail" = "email"
WHERE "newEmail" IS NULL;
`;
// ステップ3: NOT NULL制約を追加
model User {
id String @id
newEmail String @unique // NOT NULL制約
}
npx prisma migrate dev --name make_new_email_required
テスト戦略
ユニットテスト(モック)
// __tests__/user.test.ts
import { prismaMock } from '../lib/prisma-mock';
describe('User Service', () => {
it('should create a user', async () => {
const user = { id: '1', email: 'test@example.com', name: 'Test' };
prismaMock.user.create.mockResolvedValue(user);
const result = await createUser({ email: 'test@example.com', name: 'Test' });
expect(result).toEqual(user);
expect(prismaMock.user.create).toHaveBeenCalledWith({
data: { email: 'test@example.com', name: 'Test' },
});
});
});
// lib/prisma-mock.ts
import { PrismaClient } from '@prisma/client';
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';
jest.mock('./prisma', () => ({
__esModule: true,
default: mockDeep<PrismaClient>(),
}));
beforeEach(() => {
mockReset(prismaMock);
});
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>;
統合テスト(テストDB使用)
// __tests__/integration/user.test.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL,
},
},
});
beforeAll(async () => {
// マイグレーション実行
await prisma.$executeRawUnsafe('DROP SCHEMA public CASCADE; CREATE SCHEMA public;');
// execSync('npx prisma migrate deploy');
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
// 各テスト前にデータクリア
await prisma.post.deleteMany();
await prisma.user.deleteMany();
});
describe('User Integration Tests', () => {
it('should create user and profile', async () => {
const user = await prisma.user.create({
data: {
email: 'test@example.com',
name: 'Test User',
profile: {
create: {
bio: 'Test bio',
},
},
},
include: { profile: true },
});
expect(user.email).toBe('test@example.com');
expect(user.profile?.bio).toBe('Test bio');
});
});
シードデータ
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const users = await Promise.all([
prisma.user.upsert({
where: { email: 'user1@example.com' },
update: {},
create: {
email: 'user1@example.com',
name: 'User 1',
posts: {
create: [
{ title: 'Post 1', content: 'Content 1', published: true },
{ title: 'Post 2', content: 'Content 2', published: false },
],
},
},
}),
prisma.user.upsert({
where: { email: 'user2@example.com' },
update: {},
create: {
email: 'user2@example.com',
name: 'User 2',
posts: {
create: [
{ title: 'Post 3', content: 'Content 3', published: true },
],
},
},
}),
]);
console.log({ users });
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
// package.json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
# シード実行
npx prisma db seed
本番運用のベストプラクティス
コネクションプール
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
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;
ロギング
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
{ level: 'warn', emit: 'stdout' },
],
});
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Duration: ' + e.duration + 'ms');
});
ミドルウェア
// グローバルミドルウェア
prisma.$use(async (params, next) => {
const before = Date.now();
const result = await next(params);
const after = Date.now();
console.log(`Query ${params.model}.${params.action} took ${after - before}ms`);
return result;
});
// 特定モデルのみ
prisma.$use(async (params, next) => {
if (params.model === 'User') {
// ユーザーデータを暗号化
if (params.action === 'create' || params.action === 'update') {
params.args.data.email = encrypt(params.args.data.email);
}
}
return next(params);
});
バックアップ戦略
# PostgreSQLバックアップ
pg_dump $DATABASE_URL > backup.sql
# 復元
psql $DATABASE_URL < backup.sql
# 自動バックアップ(cron)
0 2 * * * pg_dump $DATABASE_URL | gzip > /backups/db-$(date +\%Y\%m\%d).sql.gz
パフォーマンス監視
Prisma Studio
npx prisma studio
# → http://localhost:5555 でGUIが起動
クエリ分析
// クエリの詳細を取得
prisma.$on('query', (e) => {
console.log('Query:', e.query);
console.log('Params:', e.params);
console.log('Duration:', e.duration + 'ms');
console.log('Target:', e.target);
});
EXPLAINによる最適化
// 生SQLでEXPLAIN実行
const result = await prisma.$queryRaw`
EXPLAIN ANALYZE
SELECT * FROM "User"
WHERE "email" = 'test@example.com';
`;
まとめ
Prisma ORMの高度なパターンをまとめます。
- クエリ最適化 - N+1問題対策、select/include活用
- トランザクション - インタラクティブトランザクション
- マイグレーション - ダウンタイムゼロ戦略
- テスト - モック、統合テスト、シードデータ
- 本番運用 - コネクションプール、ロギング、バックアップ
Prismaは型安全で高速、かつ開発体験に優れたORMです。この記事のパターンを活用して、本番環境でも安心して使えるデータベースレイヤーを構築しましょう。