この記事の要点
• Typed SQLで生SQLクエリにも完全な型安全性を実現(引数・戻り値の型を自動生成)
• クエリエンジン最適化、omitによるフィールド除外など日常開発の利便性が向上
• 型安全なクエリビルダーから「型安全なデータアクセス層」への進化
Prisma 6の概要
Prisma 6は、Typed SQLの導入により、生SQLクエリにも型安全性をもたらします。また、クエリパフォーマンスの改善、新しいクライアント機能が追加されています。TypeScriptエコシステムで圧倒的なシェアを持つORMであるPrismaが、「型安全なクエリビルダー」から「型安全なデータアクセス層」へと進化するメジャーリリースです。
背景 - Prismaが目指すもの
PrismaはTypeScriptの型システムとデータベーススキーマを橋渡しするORMとして広く採用されてきました。しかし実務では、複雑な集計・CTE・ウィンドウ関数・DB固有の機能を使いたい場面で、どうしても生SQL($queryRaw)を書かざるを得ませんでした。ここで失われがちなのが「型安全性」です。
Prisma 6の最大の目玉であるTyped SQLは、この課題を正面から解決します。SQLファイルをそのまま書き、それに対する引数と戻り値の型を自動生成することで、クエリビルダーとSQLの良いとこ取りが可能になりました。
加えて、クエリエンジンの最適化、omitによるフィールド除外、relationLoadStrategyの明示指定など、日常的な開発で嬉しい機能も多数追加されています。
Typed SQL
生SQLクエリに対して完全な型安全性を提供します。
// sql/getUserWithPosts.sql
-- @param {Int} $1:userId
SELECT
u.id,
u.name,
u.email,
json_agg(p.*) as posts
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
WHERE u.id = $1
GROUP BY u.id;
// 生成された型安全な関数
import { getUserWithPosts } from '@prisma/client/sql';
const result = await prisma.$queryRawTyped(
getUserWithPosts(123)
);
// resultは完全に型付けされている
console.log(result.name); // string
console.log(result.posts); // Post[]
Typed SQLのワークフロー
Typed SQLは以下のフローで動作します。
prisma/sql/ディレクトリに.sqlファイルを配置-- @paramコメントでパラメータ型を宣言prisma generate --sqlで型を自動生成- アプリコードから型安全に呼び出し
# 型生成コマンド
npx prisma generate --sql
# watchモードで自動再生成
npx prisma generate --sql --watch
複雑なクエリの例
-- prisma/sql/getRevenueReport.sql
-- @param {String} $1:startDate
-- @param {String} $2:endDate
-- @param {Int} $3:limit
WITH monthly AS (
SELECT
date_trunc('month', created_at) AS month,
customer_id,
SUM(amount) AS revenue
FROM orders
WHERE created_at BETWEEN $1::timestamp AND $2::timestamp
GROUP BY 1, 2
),
ranked AS (
SELECT
month,
customer_id,
revenue,
RANK() OVER (PARTITION BY month ORDER BY revenue DESC) AS rank
FROM monthly
)
SELECT
r.month,
c.name AS customer_name,
r.revenue,
r.rank
FROM ranked r
JOIN customers c ON c.id = r.customer_id
WHERE r.rank <= $3
ORDER BY r.month DESC, r.rank ASC;
import { getRevenueReport } from '@prisma/client/sql';
const report = await prisma.$queryRawTyped(
getRevenueReport('2025-01-01', '2025-12-31', 10)
);
// reportの各行は型安全
for (const row of report) {
console.log(row.month, row.customer_name, row.revenue, row.rank);
}
パフォーマンス改善
クエリエンジンの最適化
Prisma 5 vs Prisma 6 ベンチマーク:
- 単純なfindMany: 15%高速化
- リレーション含むクエリ: 25%高速化
- 大量データ取得: 30%高速化
バッチ処理の改善
// 自動バッチング
const users = await Promise.all([
prisma.user.findUnique({ where: { id: 1 } }),
prisma.user.findUnique({ where: { id: 2 } }),
prisma.user.findUnique({ where: { id: 3 } }),
]);
// 内部で1つのクエリにバッチ化
JSONプロトコルの改善
Prisma 6ではクエリエンジンとクライアント間の通信プロトコルが改善され、大量の結果を返すクエリでのシリアライズコストが削減されました。特にリレーションを含む数千行のフェッチで差が顕著に出ます。
新しいクライアント機能
omit でフィールド除外
// パスワードフィールドを除外
const user = await prisma.user.findUnique({
where: { id: 1 },
omit: {
password: true
}
});
// user.password は存在しない
グローバルomit設定で「デフォルトで常に隠す」フィールドも定義できます。
const prisma = new PrismaClient({
omit: {
user: {
password: true,
twoFactorSecret: true,
},
},
});
// ここでは常にomitされる
const user = await prisma.user.findUnique({ where: { id: 1 } });
// 明示的に必要なときだけ取り出す
const withPassword = await prisma.user.findUnique({
where: { id: 1 },
omit: { password: false },
});
relationLoadStrategy
// 明示的なリレーション読み込み戦略
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true
},
relationLoadStrategy: 'join' // または 'query'
});
join: データベースのJOINで単一クエリ実行(PostgreSQLで高速)query: 従来どおり複数クエリに分けて実行
ワークロードに応じて使い分けることで、N+1問題や過剰なJOINを避けられます。
Prisma Client Extensions 強化
const prisma = new PrismaClient().$extends({
model: {
user: {
// カスタムメソッド
async signUp(email: string, password: string) {
const hashedPassword = await hash(password);
return prisma.user.create({
data: { email, password: hashedPassword }
});
}
}
},
query: {
$allModels: {
// 全モデルに適用するフック
async $allOperations({ model, operation, args, query }) {
const start = Date.now();
const result = await query(args);
console.log(`${model}.${operation}: ${Date.now() - start}ms`);
return result;
}
}
}
});
// 使用
await prisma.user.signUp('user@example.com', 'password123');
ResultとComputed Fields
const prisma = new PrismaClient().$extends({
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`;
},
},
initials: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName[0]}${user.lastName[0]}`;
},
},
},
},
});
const u = await prisma.user.findFirstOrThrow();
console.log(u.fullName, u.initials); // 型安全
マイグレーションの改善
差分マイグレーション
# 差分を確認してからマイグレーション
prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma
# マイグレーション実行
prisma migrate dev --name add_user_profile
マイグレーションのロールバック
# 最後のマイグレーションをロールバック
prisma migrate rollback
シャドウデータベースの明示
CI/CD環境ではシャドウDB URLを明示的に渡せるようになり、マネージドDB(RDS/Cloud SQL)でのマイグレーションが容易になりました。
DATABASE_URL=... \
SHADOW_DATABASE_URL=... \
npx prisma migrate deploy
Prisma Accelerate
エッジでのコネクションプーリングとキャッシュ機能。
import { PrismaClient } from '@prisma/client/edge';
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient().$extends(withAccelerate());
// キャッシュ付きクエリ
const users = await prisma.user.findMany({
cacheStrategy: {
ttl: 60,
swr: 120
}
});
Prisma Pulse(リアルタイムクエリ)
Prisma PulseはDBの変更をリアルタイムで購読できるサービスで、Prisma 6のクライアントから利用しやすくなりました。
const subscription = await prisma.user.subscribe({
create: {},
});
for await (const event of subscription) {
console.log('New user created:', event.created);
}
実践的なサンプルコード
Next.js App Routerでの利用例
// 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'],
omit: {
user: { password: true },
},
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getUserWithPosts } from '@prisma/client/sql';
export async function GET(
_req: Request,
{ params }: { params: { id: string } },
) {
const id = Number(params.id);
if (Number.isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
const result = await prisma.$queryRawTyped(getUserWithPosts(id));
if (result.length === 0) {
return NextResponse.json({ error: 'Not Found' }, { status: 404 });
}
return NextResponse.json(result[0]);
}
トランザクションとインタラクティブAPI
const result = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'new@example.com', name: 'New User' },
});
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: 'Hello Prisma 6',
},
});
await tx.auditLog.create({
data: {
actorId: user.id,
action: 'USER_SIGNUP',
metadata: { email: user.email },
},
});
return { user, profile };
}, {
isolationLevel: 'Serializable',
timeout: 10_000,
});
バージョン比較表
| 機能 | Prisma 4 | Prisma 5 | Prisma 6 |
|---|---|---|---|
| Typed SQL | 不可 | 不可 | あり |
omitフィールド | 不可 | 不可 | あり |
relationLoadStrategy | 固定 | 限定プレビュー | 安定版 |
| Client Extensions | プレビュー | 安定版 | 拡張強化 |
| JSONプロトコル | 不可 | プレビュー | デフォルト |
| Node.js最小要件 | 14 | 16 | 18 |
| Accelerate連携 | 限定 | 対応 | 強化 |
| Pulse連携 | なし | プレビュー | 対応 |
他ORMとの簡易比較
| 観点 | Prisma 6 | Drizzle ORM | TypeORM | Kysely |
|---|---|---|---|---|
| 型安全性 | 非常に強い | 非常に強い | 強い | 強い |
| 生SQLの型 | Typed SQL | SQL構築DSL | 部分的 | クエリビルダー |
| マイグレーション | 独自ファイル | drizzle-kit | CLI | なし |
| 学習曲線 | 穏やか | 穏やか | やや急 | 穏やか |
| エッジ対応 | Accelerate経由 | ネイティブ | 不向き | 対応 |
ベストプラクティス
1. クエリ戦略の使い分け
単純なCRUDはPrisma Client、集計や複雑な結合はTyped SQLで、と使い分けましょう。両方を同じ型システムで扱えるのがPrisma 6の強みです。
2. omitでセンシティブデータを常に隠す
password、token、secretなどはグローバルomitで常時除外し、明示的に必要なときだけ取り出す運用にすると安全です。
3. コネクションプーリング
サーバーレス環境ではPrisma Accelerate、または外部のPgBouncerを使ってコネクション数を制御しましょう。Lambda/Vercel Functionsから直接接続するとDBが枯渇します。
4. スキーマは単一の真実の源
prisma/schema.prismaを中心にモデルを定義し、TypeScriptの型はすべてここから派生させましょう。重複した型定義を避けることで保守性が上がります。
5. Prismaのログをモニタリングに統合
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'error' },
{ emit: 'event', level: 'warn' },
],
});
prisma.$on('query', (e) => {
metrics.histogram('db.query.duration', e.duration, {
model: e.target,
});
});
注意点・落とし穴
- Typed SQLはビルド時生成: スキーマやSQLファイルを変更したら必ず
prisma generate --sqlを再実行する - relationLoadStrategy: ‘join’の副作用: 深いネストや重複行が多いクエリでは
queryの方が速い場合がある $queryRawとの違い: Typed SQLは.sqlファイル経由のみ。動的にSQLを組み立てたい場合は従来の$queryRawを使う- Node.js 18未満はサポート外: Prisma 6からNode.js 18以上が必須
- Edge Runtime対応は制限あり: Vercel EdgeやCloudflare Workersでは
@prisma/client/edgeとAccelerate/Data Proxy経由が前提 - マイグレーション差分検出の限界: カラム名の変更は「削除+追加」として検出されがち。手動でSQLを編集する必要がある場合あり
導入・移行手順
# Prisma 6にアップグレード
npm install prisma@latest @prisma/client@latest
# クライアント再生成
npx prisma generate
破壊的変更
| 変更 | 対応 |
|---|---|
| Node.js 18以上が必須 | Node.jsをアップグレード |
| 一部のデフォルト値変更 | 明示的に設定 |
| JSONプロトコルがデフォルト | 従来のGraphQLプロトコルは__internalで指定可 |
| 一部の暗黙的な型が厳密化 | 型エラーを修正 |
Typed SQLの導入手順
# 1. sqlディレクトリを作成
mkdir -p prisma/sql
# 2. SQLファイルを配置
cat > prisma/sql/findActiveUsers.sql <<'EOF'
-- @param {Int} $1:limit
SELECT id, name, email
FROM users
WHERE active = true
ORDER BY created_at DESC
LIMIT $1;
EOF
# 3. 型を生成
npx prisma generate --sql
# 4. 利用
# import { findActiveUsers } from '@prisma/client/sql';
# const users = await prisma.$queryRawTyped(findActiveUsers(20));
チーム移行のチェックリスト
- Node.js 18以上にアップグレード
-
prismaと@prisma/clientを同じバージョンに揃える - CIパイプラインに
prisma generate(SQLあり)を組み込む - ログレベルと監視設定を再確認
- 既存の
$queryRawをTyped SQLへ段階的に置換 -
omitで隠すフィールドを洗い出す
パフォーマンス/ベンチマーク
Prisma公式の発表に基づく代表的なワークロードでの改善幅の目安です。
| ワークロード | Prisma 5比 |
|---|---|
| 単純なfindMany(1000行) | 約15%高速化 |
| 1対多リレーションのinclude | 約25%高速化 |
| 集計クエリ(groupBy) | 約20%高速化 |
| 大量のcreateMany | 約10%高速化 |
| 小規模なfindUnique | 数%改善 |
注意として、ベンチマークはネットワークレイテンシやDBハードウェアに強く依存します。自環境でautocannonやk6を使って実測することを推奨します。
# k6でAPIエンドポイントをベンチ
k6 run --vus 50 --duration 60s loadtest.js
FAQ
Q1: Typed SQLと$queryRawはどう使い分けますか?
A: 事前にクエリが確定しているものはTyped SQLで型安全性を得ます。動的にクエリを組み立てる必要がある場合は$queryRawやPrisma.sqlテンプレートを使います。
Q2: PrismaはDrizzle ORMと比べて遅いと聞きますが、Prisma 6で変わりましたか?
A: クエリエンジンの最適化とJSONプロトコルにより、日常的なクエリでの差は縮まりました。ただしDrizzleは「薄いクエリビルダー」としての性格が強く、超低オーバーヘッドが必要な場合はDrizzleが優位な場面もあります。開発体験・マイグレーション・エコシステムの総合力ではPrismaが引き続き有力です。
Q3: relationLoadStrategyは何を基準に選べばいい?
A: 目安として、(1) リレーション先が小〜中規模ならjoin、(2) 深いネストや大きな配列を持つリレーションならquery、(3) DBがMySQLの場合はJOIN最適化が弱いのでqueryの方が安全です。
Q4: サーバーレス環境での推奨構成は?
A: Prisma Accelerateを経由するのが最も手軽です。自前で管理する場合はPgBouncerなどのコネクションプーラーを挟み、connection_limitを低めに設定しましょう。
Q5: Prisma 6でEdge Runtimeはフルサポートされますか?
A: @prisma/client/edgeとAccelerate/Data Proxy経由での利用が前提です。ネイティブドライバをEdgeで直接使うことは依然として制限があります。
まとめ
Prisma 6は、Typed SQLによる生SQLの型安全性、パフォーマンス改善、新しいクライアント機能により、より強力なORMへと進化しました。特にTyped SQLは、複雑なクエリを書く必要がある場合に非常に有用です。
- 開発者体験:
omit、relationLoadStrategy、Typed SQLで「書きたいように書ける」選択肢が広がった - 実行性能: クエリエンジン最適化とJSONプロトコルで日常的なワークロードが軽量化
- エンタープライズ: Accelerate/Pulseとの統合でエッジ・リアルタイム用途にも対応
TypeScript + PostgreSQL/MySQLのスタックを使っているチームにとって、Prisma 6は「更新して損のない」リリースです。既存プロジェクトは段階的に、新規プロジェクトはTyped SQLを前提に設計することで、型安全なデータアクセス層をシンプルに保てます。