Prisma 6の新機能 - TypeSafeなORMがさらに進化

2025.12.12

公式ドキュメント

この記事の要点

• 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は以下のフローで動作します。

  1. prisma/sql/ディレクトリに.sqlファイルを配置
  2. -- @paramコメントでパラメータ型を宣言
  3. prisma generate --sqlで型を自動生成
  4. アプリコードから型安全に呼び出し
# 型生成コマンド
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 4Prisma 5Prisma 6
Typed SQL不可不可あり
omitフィールド不可不可あり
relationLoadStrategy固定限定プレビュー安定版
Client Extensionsプレビュー安定版拡張強化
JSONプロトコル不可プレビューデフォルト
Node.js最小要件141618
Accelerate連携限定対応強化
Pulse連携なしプレビュー対応

他ORMとの簡易比較

観点Prisma 6Drizzle ORMTypeORMKysely
型安全性非常に強い非常に強い強い強い
生SQLの型Typed SQLSQL構築DSL部分的クエリビルダー
マイグレーション独自ファイルdrizzle-kitCLIなし
学習曲線穏やか穏やかやや急穏やか
エッジ対応Accelerate経由ネイティブ不向き対応

ベストプラクティス

1. クエリ戦略の使い分け

単純なCRUDはPrisma Client、集計や複雑な結合はTyped SQLで、と使い分けましょう。両方を同じ型システムで扱えるのがPrisma 6の強みです。

2. omitでセンシティブデータを常に隠す

passwordtokensecretなどはグローバル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ハードウェアに強く依存します。自環境でautocannonk6を使って実測することを推奨します。

# k6でAPIエンドポイントをベンチ
k6 run --vus 50 --duration 60s loadtest.js

FAQ

Q1: Typed SQLと$queryRawはどう使い分けますか?

A: 事前にクエリが確定しているものはTyped SQLで型安全性を得ます。動的にクエリを組み立てる必要がある場合は$queryRawPrisma.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は、複雑なクエリを書く必要がある場合に非常に有用です。

  • 開発者体験: omitrelationLoadStrategy、Typed SQLで「書きたいように書ける」選択肢が広がった
  • 実行性能: クエリエンジン最適化とJSONプロトコルで日常的なワークロードが軽量化
  • エンタープライズ: Accelerate/Pulseとの統合でエッジ・リアルタイム用途にも対応

TypeScript + PostgreSQL/MySQLのスタックを使っているチームにとって、Prisma 6は「更新して損のない」リリースです。既存プロジェクトは段階的に、新規プロジェクトはTyped SQLを前提に設計することで、型安全なデータアクセス層をシンプルに保てます。

参考リンク

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る