メインコンテンツへスキップ

Prisma 8を本番で使ってみた:導入から型安全なSQL、Supabase・pgvectorまで

Prisma 8を複数プロダクトで本番運用した実践記。TypeScriptで刷新された新アーキテクチャ、日常のCRUDと共存する型安全なSQLビルダー、Supabase Authやpgvector連携、Drizzleとの選定基準まで徹底解説します。

複数のプロダクトにおいて、Prisma 8の運用を開始しました。単純なCRUD操作から、Supabase Authとアプリケーションテーブルの紐付け、さらにはpgvectorによるベクトル検索に至るまで、要件に応じて幅広く活用しています。いずれもすでに一般公開され、本番環境で安定して稼働しているプロダクトです。

本記事では、PostgreSQLとTypeScriptを用いた環境への新規導入手順をはじめ、基本的なCRUD操作、新設された型安全なSQLクエリビルダー、マイグレーション管理、そしてSupabaseやpgvectorといった拡張機能の活用法までを網羅的に解説します。その上で、前バージョンであるPrisma 7からの進化点や、Drizzle等の他ツールと比較した際の選定基準についても整理します。

従来のPrisma 7では、動的な検索条件や集約処理を実装する過程で「ここからはORMではなく生SQLで直接記述したほうが早い」と感じる場面がしばしばありました。Prisma 8では、そうした場面でも思考や作業が中断されることが格段に少なくなりました。PythonのSQLAlchemyを想起させるような、SQLを式として柔軟に組み上げる自由度があり、そこにTypeScriptならではの入力補完と戻り値の型推論が一体となって寄り添う感覚です。

個人的に最も評価しているのは、 日常的なCRUD操作と、SQLの構造を細かく制御したい複雑なクエリを、同一のデータ定義からシームレスに書き分けられる点 です。この使い分けの心地よさをお伝えできるよう、基本操作から順を追って見ていきます。

なお、掲載しているコード例は実務での設計パターンをもとに、架空の読書管理サービスを題材として再構成したものです。サービス名やモデル名、テーブル名、カラム名などは実在のものとは異なります。アプリケーション側のテーブルは public スキーマに統一し、ライブラリの公式API名やSupabase Authの参照名は仕様に合わせてそのまま使用しています。

本記事は2026年10月16日時点の情報に基づいています。Prisma 8は執筆時点でRC版(リリース候補版)であり、参照した最新のORMバージョンは 8.0.0-rc.18 です。CLIツールである prisma とORMランタイムパッケージは個別にバージョン管理されるため、RC番号が一致しない場合があります。また、本記事の解説はPostgreSQLを対象としています。公式のリリース状況、ORM rc.18のリリースノート

Prisma 8の中心にあるのは、contractと共通の実行基盤

Prisma 8は、コアエンジンを含めTypeScriptで全面的に再設計されたORMです。スキーマ定義を先行させる(Schema-first)という基本思想は維持されていますが、スキーマからアプリケーション固有の巨大なクライアントコードを一括生成していた従来の仕組みは刷新されました。公式リポジトリ

Prisma 8では、モデルやフィールド、リレーション、データベースとのマッピング関係を定義するファイルを「contract」と呼びます。従来のPrismaに近いPSL(Prisma Schema Language)で記述する場合は contract.prisma、TypeScriptで直接記述する場合は contract.ts を使用します。本記事では、従来のPrismaユーザーにも馴染みやすいPSLを採用して解説を進めます。The data contract

contractをもとにCLIによって生成される成果物は、主に以下の2点です。

ファイル

役割

contract.json

モデルとDB構造のマッピング情報や有効化された機能などを記録したメタデータ。実行時ランタイムおよびCLIが参照します

contract.d.ts

contractから導出されたTypeScript型定義。入力補完・カラム参照・クエリ結果の型推論の基盤となります

これらの成果物は、データベースごとに提供される共通ランタイムと組み合わせて動作します(PostgreSQLの場合は @prisma/orm-postgres)。モデルごとに肥大化しがちだったクライアントコード全体を生成する方式から、構造のメタデータと型情報のみを生成し、共通のランタイム基盤上で駆動させるアーキテクチャへと移行しています。contractの生成物

また、クエリを記述するAPIの入り口も、開発の目的に応じて選べるようになりました。

API

主な用途

クエリの実行方法

db.orm

モデル単位のCRUD操作、関連データの階層的な取得

.all()、.first()、.create() など直接メソッドを呼び出します

db.sql

JOIN、集約、サブクエリ、計算列などを伴う型安全なクエリ構築

.build() で生成した実行計画(plan)を query() または execute() に渡します

db.raw.sql

SQL文全体を直接手書きしたい特殊な処理

戻り値の型構造を宣言して plan を作成し、ランタイムに渡します

いずれのAPIも同一のcontractに基づいており、内部的には「plan」と呼ばれるクエリの中間表現を通じて安全に実行されます。普段はORMの簡潔さを享受しつつ、要件に応じてスムーズにSQLビルダーへと移行できる設計になっています。Core concepts

PostgreSQLの空の開発DBに導入する

まずは新規プロジェクトへの導入手順を説明します。ここでは、既存テーブルが存在しない空の開発用PostgreSQLと、package.json を備えたTypeScriptプロジェクトを前提とします(既存DBへの導入やPrisma 7からの移行手順は後述します)。

実行環境を確認する

公式ドキュメントで推奨されている実行要件は、Node.js 22系(22.18以降)、24系(24.11以降)、または26系以降、TypeScriptは5.9以降です。PostgreSQLはバージョン15以降が推奨されています。本記事ではNode.js 24系とnpmを使用し、TypeScriptスクリプトの実行環境には tsx を採用します。必要な実行環境、PostgreSQLの導入ガイド

npm install --save-dev typescript tsx
npx prisma@latest orm init --yes --target postgres --authoring psl --write-env

執筆時点では、公式の latest タグがPrisma 8のRC版を指しています。orm init コマンドを実行すると、必要な依存パッケージのインストールをはじめ、contractファイル、設定ファイル、クライアント初期化コードの雛形が一括して生成されます。なお、Prisma 8では単なる prisma init はORM用ではなくAIエージェント向けツールの初期化コマンドとなっているため、ORMをセットアップする際は必ず prisma orm init を実行してください。また、このコマンドによって package.json や tsconfig.json も適切に更新されます(既存のCommonJSプロジェクトで導入する場合は、公式設定手順も併せてご確認ください)。orm init

本記事の構成における主要なファイル配置は以下のとおりです。

prisma.config.ts
src/
  prisma/
    contract.prisma
    contract.json      # contract emit で生成
    contract.d.ts      # contract emit で生成
    db.ts
.env

実行時ランタイムには @prisma/orm-postgres を用い、CLIツールには開発用依存関係(devDependencies)の prisma を使用します。従来の @prisma/client は使用しません。CLIとORMパッケージのバージョン番号はそれぞれ独立して進められるため、セットアップ後はパッケージマネージャーのロックファイル(package-lock.json など)で確実にバージョンを固定・管理することをおすすめします。インストールするパッケージ

接続先とCLIの設定を書く

.env に開発用データベースの接続文字列を設定します(以下の接続情報はサンプルです)。

DATABASE_URL="postgresql://demo_reader:demo_password@localhost:5432/reading_demo"

設定ファイルである prisma.config.ts には、contractファイルのパス、データベース接続情報、マイグレーションファイルの出力先を指定します。

import "dotenv/config";
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";

const connection = process.env.DATABASE_URL;
if (!connection) throw new Error("DATABASE_URL is required");

export default definePrismaConfig({
  orm: ormConfig({
    contract: "./src/prisma/contract.prisma",
    db: { connection },
    migrations: { dir: "./migrations" },
  }),
});

対象データベースの種類は、@prisma/orm-postgres/config の設定ヘルパー(ormConfig)を利用することでPostgreSQLとして確定します。そのため、Prisma 7までのようにcontract内へ datasource や generator ブロックを記述する必要はありません。なお、初期化時のテンプレートによっては definePrismaConfig のimport元が異なる場合もありますが、すでに生成されているファイルがある場合は、そのimport構成を活かして必要な設定項目を追記してください。CLI configuration、初期化時の設定ファイル

モデルとテーブルの対応を定義する

src/prisma/contract.prisma を作成し、読者(ReaderAccount)と読書リスト(ReadingList)の2つのモデルを定義します。

// use prisma-8

model ReaderAccount {
  accountKey String        @id @default(uuid()) @map("account_key")
  handle     String        @unique
  lists      ReadingList[]

  @@map("reader_accounts")
}

model ReadingList {
  listKey   String        @id @default(uuid()) @map("list_key")
  title     String
  ownerKey  String        @map("owner_key")
  isVisible Boolean       @default(false) @map("is_visible")
  owner     ReaderAccount @relation(fields: [ownerKey], references: [accountKey])

  @@index([ownerKey])
  @@map("reading_lists")
}

先頭行の // use prisma-8 はPrisma 8のPSLであることを明示する必須ディレクティブです。手動でファイルを作成する場合も必ず記載してください。@map はモデルのフィールド名とDBカラム名、@@map はモデル名とDBテーブル名のマッピングを指定します。この定義により、ORM(db.orm)からはキャメルケースの ReadingList や ownerKey として扱い、SQLビルダー(db.sql)からはデータベースそのままのスネークケース reading_lists や owner_key として扱うことができます。1つのcontractにこのマッピング関係を集約できるため、2つのAPIを違和感なく使い分けられます。PSL syntax、モデル名と保存名

contractを生成し、テーブルを作る

定義が完了したら、以下のコマンドを実行します。

npx prisma contract emit
npx prisma db init

1つ目のコマンドで型定義とメタデータJSONを生成し、2つ目のコマンドで開発用データベースにテーブルを作成します。なお、この手順は空のデータベースを前提としており、すでにPrisma 7等で稼働しているデータベースに対してそのまま実行してはいけません(既存DBへの導入方法は後述します)。db init

生成された contract.json と contract.d.ts は手動で編集せず、ソースコードとともにGitなどのバージョン管理対象に含めます。特に contract.json は実行時ランタイムでも参照されるため、本番環境のビルド・デプロイ成果物にも必ず含める必要があります。Prisma 8で生成されるのは純粋なTypeScript型定義とJSONメタデータのみであり、以前のようにプラットフォーム別のバイナリエンジン(Query Engine)をデプロイ先ごとに用意する必要はありません。生成物の管理

クライアントを用意して最初のクエリを実行する

データベースクライアントの初期化コード src/prisma/db.ts は以下のようになります。

import "dotenv/config";
import postgres from "@prisma/orm-postgres/runtime";
import type { Contract } from "./contract.d";
import contractJson from "./contract.json" with { type: "json" };

const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");

export const db = postgres<Contract>({
  contractJson,
  url,
});

./contract.d からの型インポートは、先ほど生成した contract.d.ts を参照しています。続いて、動作確認用のスクリプト script.ts をプロジェクト直下に作成し、データの作成と一覧取得を試してみます。

import { db } from "./src/prisma/db";

async function main() {
  try {
    const account = await db.orm.public.ReaderAccount.create({
      handle: "sample_reader",
    });

    await db.orm.public.ReadingList.create({
      title: "今月読みたい本",
      ownerKey: account.accountKey,
    });

    const lists = await db.orm.public.ReadingList
      .where({ ownerKey: account.accountKey })
      .all();

    console.log(lists);
  } finally {
    await db.close();
  }
}

main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

スクリプトを実行します。

npx tsx script.ts

Prisma 8の create メソッドでは、従来の { data: ... } で包む記法ではなく、プロパティを直接オブジェクトとして渡します。なお、上記のコードは初回確認用のため、同一の handle で再実行するとユニーク制約違反のエラーになります。また、単発のスクリプトでは末尾で db.close() を呼んでコネクションを解放しています(Webサーバーのリクエストごとに毎回インスタンスを生成・切断する設計ではありません)。Writing data、最初のクエリ

これで、モデル定義から型とメタデータの生成、データベースへのテーブル作成、そして最初のデータアクセスまでの一連のパイプラインが開通しました。以降のクエリ例では、db のimportや関数の引数などの定型コードは適宜省略して記載します。

普段のCRUDは、引き続き短く書ける

まずは、日常的な基本データアクセスの使い勝手を見ていきます。利用者が作成した読書リストの一覧を取得する場合、以下のようにシンプルに記述できます。

const readingLists = await db.orm.public.ReadingList
  .select("listKey", "title", "ownerKey")
  .where((list) => list.ownerKey.eq(ownerKey))
  .all();

// readingLists: { listKey: string; title: string; ownerKey: string }[]

1件のみを取得する場合は以下のようになります。

const reader = await db.orm.public.ReaderAccount
  .where((account) => account.handle.eq(handle))
  .first();

フィールド名や引数の型がエディタ上でスムーズに補完され、選択したカラムに応じて戻り値の型が自動で推論されます。構文自体は従来のオブジェクト形式からメソッドチェーン形式へと刷新されましたが、スキーマ定義を基盤として型安全にアクセスできるというPrisma本来の魅力は損なわれていません。ORM client reference

なお、.first() は該当するレコードが存在しない場合に null を返します。Prisma 7における findUnique と findFirst は、いずれもこの .first() に統合されました。一意な主キーによる検索か、複数件のうち先頭1件を取り出す検索かは、条件式およびデータベース側の制約によって決まります。後者において意図したレコードを確実に取得したい場合は、ソート順(.orderBy)も明示してください。Reading data

更新・削除は、絞り込んでから実行する

データの更新や削除は、.where() で対象を特定してから実行します。

const updated = await db.orm.public.ReadingList
  .where({ listKey })
  .update({ title: "週末に読む本", isVisible: true });

const removed = await db.orm.public.ReadingList
  .where({ listKey })
  .delete();

単一レコードを操作する update / delete と、複数レコードを一括処理する updateAll / deleteAll が明確に区別されています。条件指定と実際の更新処理がメソッドチェーンで直感的に分離されたため、Prisma 7の深くネストしたオブジェクト記法に比べてコードの見通しが良くなっています。更新と削除

関連データはincludeでまとめて取得する

リレーション関係にある関連データをまとめて取得したい場合は、include を使用します。

const listsWithOwner = await db.orm.public.ReadingList
  .where({ isVisible: true })
  .select("listKey", "title")
  .include("owner")
  .limit(20)
  .all();

owner はcontractで定義したリレーションフィールド名です。取得結果には、指定したリストのカラム群に加え、関連する所有者情報(ReaderAccount)のオブジェクトがネストされた形で型安全に含まれます。モデルを中心としたWeb画面の表示要件であれば、まずはこの include を活用することで、無駄に手動でJOINを構築する工数を削減できます。Relations and joins

注意点として、.all() は全件取得を試みるため、自動で取得件数の制限は行われません。実務では .limit(20) のように取得上限を指定したり、.orderBy((list) => list.title.asc()) で並び順を明示したりするのが基本です。ソートとページング

複数の書き込みはtransactionでまとめる

複数の書き込み操作を不可分(アトミック)に実行したい場合は、db.transaction を使用します。

const createdList = await db.transaction(async (tx) => {
  const account = await tx.orm.public.ReaderAccount.create({ handle });

  return tx.orm.public.ReadingList.create({
    title: "最初の読書リスト",
    ownerKey: account.accountKey,
  });
});

コールバック関数内では、引数として渡される tx を通じて操作を行います。この中で誤って外部の db.orm を呼び出してしまうと、現在進行中のトランザクションのスコープから外れてしまうため注意が必要です。また、後述するSQLビルダーで構築した実行計画(plan)をトランザクション内で実行する場合も、tx.query(plan) や tx.execute(plan) に渡して実行します。Transactions

複雑な検索を、TypeScriptの中で組み立てやすくなった

実務においてPrisma 8の恩恵を最も強く実感できたのが、動的なクエリ構築のしやすさです。実際のプロダクト開発では、複数の検索キーワード、動的に選択されるカテゴリやタグ、関連テーブルの条件、ソート順の切り替えなどを柔軟に組み合わせる検索要件が日常的に発生します。条件の有無によってSQLの構造自体が変化するため、静的なSQL文をあらかじめ用意しておく手法では対応が難しくなりがちでした。

以下は、この検索パターンを書籍検索に置き換えて表現した例です。

先ほどの読書リストとは別に、検索対象となる書籍(LibraryBook)とトピックの紐付け(BookTopic)を定義したcontractを用意します。

// use prisma-8

model LibraryBook {
  bookKey      Int    @id @default(autoincrement()) @map("book_key")
  displayTitle String @map("display_title")
  shelfKey     Int    @map("shelf_key")

  @@index([shelfKey])
  @@map("library_books")
}

model BookTopic {
  bookKey  Int @map("book_key")
  topicKey Int @map("topic_key")

  @@id([bookKey, topicKey])
  @@map("book_topics")
}

※ここではサブクエリの動作解説に焦点を当てるため、リレーションフィールドの記述を最小限にしています。実務の設計では外部キー制約などを適切に設定してください。

このテーブルに対し、条件の有無に応じて動的に検索クエリを組み立てる例が以下です。

let query = db.sql.public.library_books
  .select("book_key", "display_title");

if (shelfKeys.length > 0) {
  query = query.where((f, fns) => fns.in(f.shelf_key, shelfKeys));
}

const plan = query
  .orderBy("book_key", { direction: "desc" })
  .limit(20)
  .build();

const rows = await db.runtime().query(plan);

select や where などのメソッドチェーンでクエリの骨格を組み立て、最後に .build() を呼ぶことでクエリの実行計画オブジェクト(plan)を生成します。この plan を db.runtime().query(plan) に渡すことで、初めてデータベースへ実際の問い合わせが発行されます(プレースホルダーへのパラメータバインドは自動かつ安全に行われます)。なお、トランザクション内では tx.query(plan) を使用します。SQL query builder reference

上記のコードでは、本棚IDの配列 shelfKeys に値が存在する場合にのみ、IN 条件をクエリに追加しています。文字列連結による脆弱なSQL構築に頼ることなく、TypeScriptの通常の if 文を用いて安全かつ柔軟に条件を組み立てられるのが特長です。コールバック引数の f はテーブルのカラム参照オブジェクト、fns は eq、in、and などのSQL演算子を生成する関数群です。

この構文は、ORMの .where((list) => list.ownerKey.eq(...)) とも対比できます。ORMがモデルのプロパティ単位で条件を記述するのに対し、SQLビルダーではデータベースのカラム参照とSQL演算子を組み合わせて記述します。どちらも直感的なメソッドチェーンですが、「モデル層を扱っているのか、テーブル・SQL層を扱っているのか」というレイヤーの違いを意識すると、より自然に書き分けられます。

さらに、関連テーブルを用いたサブクエリ(副問合せ)も自然に組み込むことができます。たとえば、特定のトピックに属する書籍のみをサブクエリで絞り込む場合は、以下のように記述します。

query = query.where((f, fns) =>
  fns.in(
    f.book_key,
    db.sql.public.book_topics
      .select("book_key")
      .where((link, ops) => ops.in(link.topic_key, topicKeys)),
  ),
);

これは .build() を呼び出す前に動的に追加できる条件です。topicKeys に絞り込み条件が存在する場合にのみ if (topicKeys.length > 0) でこの条件ブロックを追加する、といった実装も容易です。

TypeScriptの制御フロー(if や配列操作)とSQLの構文要素を同一の文脈でシームレスに扱えるため、開発時のストレスが大幅に減りました。実務でも、複雑な絞り込みはSQLビルダーでID一覧を抽出し、画面表示用の詳細データはORMの include で取得するなど、要件に応じた柔軟な連携を行っています。

誤解のないよう補足すると、Prisma 7でも AND / OR やリレーションフィルターを活用することで、複雑な条件検索自体は実現可能でした。Prisma 8で向上したのは、JOINやサブクエリ、計算列、射影(プロジェクション)などを、生のSQLに近い直感的な組み立て方で表現できるようになった点です。Prisma 7のフィルター

また、「意図したSQLを発行しやすくなった」という点についても、あくまで開発者が思い描くSQL構造をコード上で直接表現しやすくなったという意味であり、それだけで自動的にクエリが高速化されるわけではありません。実際のクエリ性能については、実行計画(EXPLAIN)やインデックス設計を検証した上で確認する必要があります。

JOIN・集計・結果型もSQLビルダーで扱う

JOINは、返したい一行の形を明示する

先ほどの読書リストと読者アカウントを明示的にJOINし、所有者の handle を含めたフラットな1行のデータとして取得してみます。

const listPlan = db.sql.public.reading_lists
  .as("l")
  .innerJoin(db.sql.public.reader_accounts.as("r"), (f, fns) =>
    fns.eq(f.l.owner_key, f.r.account_key),
  )
  .select((f) => ({
    listKey: f.l.list_key,
    title: f.l.title,
    ownerHandle: f.r.handle,
  }))
  .where((f, fns) => fns.eq(f.l.is_visible, true))
  .limit(20)
  .build();

const rows = await db.runtime().query(listPlan);
// { listKey: string; title: string; ownerHandle: string }[]

.as(...) でテーブルにエイリアスを付与し、射影(select)のコールバック内で返却するオブジェクトのプロパティを定義します。ネストした階層構造で関連レコードを取得したい場合は db.orm の include を使い、複数テーブルから必要なカラムのみを結合したフラットなレコードを取得したい場合は db.sql の明示的なJOINを使う、といった明確な使い分けが可能です。JOINと射影

GROUP BYと並び順をDB側で組み合わせる

集約処理についても、データベース側の機能をフルに活用できます。

const rankingPlan = db.sql.public.reading_lists
  .select((f, fns) => ({
    ownerKey: f.owner_key,
    listCount: fns.count(),
  }))
  .where((f, fns) => fns.eq(f.is_visible, true))
  .groupBy((f) => f.owner_key)
  .orderBy((f, fns) => fns.count(), { direction: "desc" })
  .limit(5)
  .build();

const ranking = await db.runtime().query(rankingPlan);

これは、公開リスト数の多い上位5名のユーザーを集計して取得する例です。アプリケーション側で全レコードを取得してメモリ上でカウントするのではなく、データベース側で GROUP BY と ORDER BY による集約処理を行わせることができます。なお、count() はJavaScriptの number 型として返されますが、集計件数が膨大になる可能性がある場合は countBigInt() を使用して bigint として安全に受け取ることも可能です。集計関数

クエリの結果型を関数の境界へ持ち出す

SQLビルダーで構築したクエリの結果型は、ユーティリティ型 ResultType を使って簡単に抽出できます。

import type { ResultType } from "@prisma/orm-postgres/components/runtime";

type ListSummary = ResultType<typeof listPlan>;

function getTitles(rows: ListSummary[]) {
  return rows.map((row) => row.title);
}

ResultType<typeof listPlan> はクエリ結果の1行分のオブジェクト型を表し、query(listPlan) の実行結果はその配列(ListSummary[])として厳密に型付けされます。SELECT句の内容に合わせて手作業でインターフェースを重複定義する必要がなくなり、取得カラムを変更した際にも型の不整合をコンパイル時に検知できます。ResultType

書き込みでは返却行の有無に応じて実行方法を選ぶ

SQLビルダーは参照クエリだけでなく、insert、update、delete および returning 句を伴う書き込み処理にも対応しています。戻り値のレコードが存在するplanには query() を使用し、行を返さない更新・削除には execute() を使用するのが基本です。後者は影響を受けた行数(affected rows)を含む実行結果オブジェクトを返します。SQLビルダーの書き込み

TypedSQLとの違いは、型が付くタイミングと組み立て方

Prisma 7にも生SQLに型を付けるアプローチとして「TypedSQL」が存在していました。これは .sql ファイルに記述したクエリから入力パラメータと戻り値の型を自動生成し、型付き関数として呼び出せる仕組みです。静的なSQLクエリを中心とした開発スタイルにおいては非常に優れた機能でした。

しかし実務においては、SQLファイルを編集するたびにデータベースへ接続可能な環境で prisma generate --sql を再実行してコード生成を待つ必要があり、開発サイクルがやや重く感じられる場面がありました。特に、先述したような「アプリケーション側のロジックで条件を動的に組み立てたいクエリ」では運用の難しさがありました。Prisma 7のTypedSQL

一方、Prisma 8のSQLビルダーでは、あらかじめ生成されたcontractのメタデータをもとに、クエリの構築と結果型の推論がTypeScriptの型チェッカ上でリアルタイムに行われます。取得カラムを変更しても即座にTypeScriptの型へ反映されるため、コード生成コマンドを都度実行することなくスムーズに開発を進められます。

ただし、クエリ内に生SQL(rawフラグメント)を部分的に埋め込んだ場合、その中身まで完全に検証されるわけではない点には注意が必要です。

const plan = db.sql.public.library_books
  .select("book_key")
  .select("caption", (f, fns) =>
    fns.raw`COALESCE(${f.display_title}, '')`.returns("pg/text@1"),
  )
  .build();

"pg/text@1" はPostgreSQLの text 型に対応する型識別子です(boolean なら "pg/bool@1")。注意点として、.returns(...) は開発者が戻り値の型を明示的に宣言する記法にすぎず、記述したSQL式そのものの構文や整合性をDBが検証してくれるわけではありません。また、rawフラグメントの .returns(...) は実行時の値変換(デシリアライズ等)を行うものでもありません。contractによって完全に型推論される領域と、開発者自身が正しさを担保すべき raw 領域は明確に区別しておく必要があります。Raw queries reference

SQL文全体を手書きしたい場合は、db.raw.sql という別の入り口も用意されています。

const rawPlan = db.raw.sql`
  SELECT book_key, display_title
  FROM public.library_books
  WHERE shelf_key = ${shelfKey}
  LIMIT ${20}
`
  .returnsRow({
    book_key: db.sql.public.library_books.columns.book_key,
    display_title: db.sql.public.library_books.columns.display_title,
  })
  .build();

const rows = await db.runtime().query(rawPlan);

.returnsRow(...) は、結果セットのカラム定義と、それらの値をどのようにデシリアライズするかをランタイムに伝えるための設定です。SQL文自体の正しさは開発者が担保する必要がありますが、カラム単位の適切な値変換が保証されます(ビルダー内のrawフラグメントで用いる .returns(...) とは役割が異なります)。なお、${...} に渡した通常の値は安全にプレースホルダーパラメータとして展開されますが、テーブル名やSQL句などの構造自体を動的にバインドできるわけではない点にも留意してください。文全体のraw SQL

個人的な使い分けの基準としては、通常のモデル操作には db.orm、動的な条件構築や結合・射影には db.sql、どうしてもビルダーで表現しきれない極めて特殊なSQLにのみ db.raw.sql を採用しています。TypedSQLのように独立したSQLファイルで管理するアプローチにも一定のメリットはありますが、都度のコード生成を挟むことなく、ビジネスロジックの分岐と同じTypeScriptコード内で完結できるPrisma 8のスタイルは非常に快適です。

スキーマ変更はemit・plan・migrateの順に進める

初期導入後も、contract.prisma を書き換えただけではデータベースに反映されません。型の再生成とデータベーススキーマの更新は、それぞれ明確に異なるステップとして進めます。

たとえば、ReadingList モデルに任意の説明文フィールドを追加する場合を考えます。

description String?

開発中にスキーマ変更をマイグレーション履歴として記録・適用する場合、以下の3ステップで行います。

npx prisma contract emit
npx prisma migration plan --name add_reading_list_description
npx prisma db migrate --advance-ref db

各コマンドの役割は以下のとおりです。

  1. prisma contract emit: contractをもとに型定義とメタデータJSONを更新します
  2. prisma migration plan: 直前の状態との差分を解析し、マイグレーションファイルを生成します
  3. prisma db migrate: 生成されたマイグレーションをデータベースに適用します

ポイントとして、migration plan コマンド自体はデータベースへ接続しません。リポジトリに記録されたベースラインと新しいcontractの差分をオフラインで比較してマイグレーションファイルを生成します。生成されたファイルの内容を開発者が確認・レビューした上で db migrate によってDBへ適用し、migrations/ ディレクトリ配下の成果物をGit等で管理します。Generating a migration

DBがどのcontractに対応するかを記録する

Prisma 8では、データベースがどのcontractの状態にあるかをハッシュ値(signature)として記録します。一方、リポジトリ側には、次回の差分マイグレーションを計画する際の起点(ベースライン)を表すポインタとして db ref が保持されます。マイグレーション実行時に --advance-ref db を指定することで、開発用DBへ変更を適用したと同時に、ローカルの db ref も最新の状態へ前進させることができます。db refと計画の開始点

マイグレーションは from と to の状態を持つ遷移として定義され、その履歴はGitのようにグラフ構造として管理されます。単一のタイムスタンプ順で直列に管理していたPrisma 7とは根本的に異なる設計です。ただし、履歴がグラフで管理されるからといって、ブランチ間で同一カラムに対する競合する変更が行われたような論理的コンフリクトまで自動解消されるわけではない点には留意が必要です。The migration graph

本番へは作成済みの変更を適用し、整合性を確認する

本番環境へのデプロイ時には、CI/CDパイプラインなどを通じてレビュー済みのマイグレーションを適用し、contractと実データベースの整合性を検証します。

npx prisma db migrate --db "$DEPLOY_DATABASE_URL"
npx prisma db verify --db "$DEPLOY_DATABASE_URL"

prisma db verify は、実データベースのスキーマとcontractのsignatureが正しく一致しているかを厳密に検証し、差分があれば終了コード1で失敗します。実はPrisma 8のランタイム自身も初回クエリ実行時にsignatureの一致をチェックしますが、ランタイム側は不一致を検知しても警告(Warning)を出力してクエリ実行を継続します。そのため、スキーマの不整合によるデプロイ事故を確実に防ぐには、CI/CDのデプロイパイプラインに db verify を組み込むことが重要です。db verify、ランタイムの確認動作

なお、マイグレーションファイルを作成せずに開発用DBのスキーマを直接contractに合わせて同期する db update コマンドも用意されています(Prisma 7の db push に近い機能です)。プロトタイピング時には便利ですが、チーム開発で本番に適用する変更については、どこからマイグレーションファイルとして記録・管理するかチーム内で運用ルールを決めておくことが大切です。db update

拡張は、型定義と実行時の演算を一緒に追加する

Prisma 8における「拡張(Extensions)」は、従来のPrisma Clientにヘルパー関数を追加する程度だった $extends とは適用範囲が全く異なります。データベース固有のカスタム型定義、その読み書きを担うコーデック(Codec)、専用のクエリ演算子、さらには拡張に必要なマイグレーションDDLまでを1つの独立したパッケージとして組み込むことができます。また、外部プラットフォームが管理するテーブル定義を取り込むことも可能です。Extensions

拡張の導入では、基本的に「CLI側の設定(prisma.config.ts)」と「アプリケーションのランタイム設定(db.ts)」の双方に登録を行います。設定側はcontractのスキーマ解釈やマイグレーション生成に使用され、ランタイム側は実行時のSQL演算やデータ型の変換処理を担当します。ここからは、実務で実際に活用しているSupabase拡張とpgvector拡張の具体例を紹介します。

補足として、スキーマファイルの分割機能自体はPrisma 7以前からサポートされていました。単なるファイル分割、PostgreSQLにおけるスキーマ分離(public や auth など)、そして外部拡張によるcontractの合成はそれぞれ異なる概念です。Prisma 8で特に優れていると感じるのは、自作アプリケーションのテーブル定義と外部サービスのテーブル定義を綺麗に合成しつつ、その管理境界を明確に維持できる設計になっている点です。Prisma 7のスキーマファイル構成

Supabaseでは、外部のauthモデルとアプリのモデルを関連付けられる

ユーザー認証基盤をSupabase Authに委託し、アプリケーション固有の業務データを public スキーマで管理する構成も広く使われています。ここでは、Supabaseプロジェクトへの接続を前提として、読書サービスの会員証(MemberCard)を例に解説します。

導入するパッケージは @prisma/orm-extension-supabase です。

npm install @prisma/orm-extension-supabase

contract.prisma にモデルを定義します。

// use prisma-8

model MemberCard {
  memberKey    Uuid                   @id @map("member_key")
  authIdentity supabase:auth.AuthUser @relation(fields: [memberKey], references: [id], onDelete: Cascade)
  displayLabel String                 @map("display_label")

  @@map("member_cards")
}

supabase:auth.AuthUser はSupabase拡張パッケージから提供される組み込みモデルです。会員証の主キー memberKey をSupabase Auth側のユーザーID(auth.users.id)に外部キーとして関連付けています。これにより、パスワードや認証資格情報をアプリ側で二重管理することなく、安全に連携できます。

設定ファイル prisma.config.ts には @prisma/orm-extension-supabase/pack を登録します。拡張が持つcontract定義と、アプリ側のcontract定義を合成するための設定です。

import supabasePack from "@prisma/orm-extension-supabase/pack";

// prisma.config.tsのormConfig(...)内へ追加する項目
extensions: [supabasePack],

Prisma 7でも、@@schema を用いたマルチスキーマ対応やスキーマ間のリレーション定義自体は可能でした。Prisma 8の特筆すべき点は、Supabaseが管理するテーブル群を拡張パッケージ経由のモデルとして安全に参照できる点にあります。Prisma 7のmulti-schema、Prisma 8の外部モデル参照構文

公式の実装では、Supabase側のテーブルは自動的に external(外部管理テーブル)として扱われるため、PrismaのマイグレーションがSupabase側のDDLを変更・削除してしまう心配がありません。自作テーブルとプラットフォーム管理のテーブルの責務を綺麗に分離できます。ただし、外部モデルへの参照が定義できることと、権限の異なる auth スキーマのテーブルを一般のDBクライアントから無制限にJOINできることは別問題である点には注意してください。Supabase拡張の公式README

なお、外部テーブルをマイグレーション対象から除外する仕組み自体はPrisma 7のPreview機能にも存在していました。Prisma 8の拡張機能が優れているのは、その定義がnpmパッケージ化されたcontractとして提供され、依存関係としてクリーンに取り込めるようになった点です。Prisma 7のexternal tables

現在の公式ドキュメントでは、Supabaseが発行するJWTとPostgreSQLの実行ロールを結び付けるための supabase(...) 関数や .asUser(jwt) といったAPIも案内されています。注意点として、前述のリレーションを定義しただけで自動的にRLS(Row Level Security)が有効化されるわけではありません。また、Supabase拡張自体も執筆時点ではExperimental(実験的機能)の位置付けです。Using extensions

現在の公式構成において、ロールを結び付けたクライアントを生成するコード例は以下のようになります(通常の postgres(...) クライアントに代わるSupabase用の初期化例です)。

import "dotenv/config";
import { supabase } from "@prisma/orm-extension-supabase/runtime";
import type { Contract } from "./contract.d";
import contractJson from "./contract.json" with { type: "json" };

const url = process.env.DATABASE_URL;
const jwksUrl = process.env.SUPABASE_JWKS_URL;
if (!url || !jwksUrl) throw new Error("Supabase configuration is required");

export const db = await supabase<Contract>({ contractJson, url, jwksUrl });

JWKS URLは、SupabaseプロジェクトのJWT署名を検証するための公開鍵の取得先です(通常は https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json の形式になります)。この構成では、db から直接クエリを発行するのではなく、リクエストごとのJWTを結び付けてクライアントを生成します。

const userDb = await db.asUser(jwt);
const cards = await userDb.orm.public.MemberCard
  .select("memberKey", "displayLabel")
  .all();

このクエリで取得される行をユーザー本人だけに制限している実体は、PostgreSQL側にあらかじめ設定されたRLSポリシーです。Supabase拡張が担う「JWTコンテキストのDBロールへの伝播」、アプリ側テーブルとの「外部キー参照」、そして「RLSポリシーによる行フィルタリング」は、それぞれ別のレイヤーとして機能しています。また、このランタイム機能のサポート環境(Node.jsやBunなど)については公式READMEをご確認ください。Supabase拡張のランタイムと権限

外部認証基盤とのリレーション関係をcontract上に宣言的に明記できるため、スキーマを読むだけでアプリケーションのデータが認証ユーザーとどのように紐付いているかが一目で把握できるようになりました。

pgvectorの型と距離計算も、Prismaの拡張として扱える

実務ではAI機能の実装に伴い、PostgreSQLの pgvector 拡張によるベクトル埋め込みをスキーマに定義する機会も増えています。ここでは、書籍の紹介文から生成した埋め込みベクトルを保存するモデルを例に解説します。

設定とランタイムの両方に登録する

npm install @prisma/orm-extension-pgvector

pgvector拡張では、CLI設定側には /control、実行時ランタイム側には /runtime をインポートして登録します。設定ファイルへの追記内容は以下のとおりです。

// prisma.config.ts
import pgvector from "@prisma/orm-extension-pgvector/control";

// ormConfig(...)内へ追加
extensions: [pgvector],
// src/prisma/db.ts
import pgvector from "@prisma/orm-extension-pgvector/runtime";

// postgres<Contract>({...})内へ追加
extensions: [pgvector],

※先ほどのSupabase拡張(/pack)とはサブパス名が異なる点にご注意ください。なお、Supabase用クライアントと併用する場合は、supabase<Contract>({...}) の extensions 配列に渡すことで共存可能です。拡張の登録方法

ベクトル型をcontractに宣言する

contractにベクトルカラムを持つモデルを定義します。

// use prisma-8

model TextVector {
  vectorKey     BigInt                @id @default(autoincrement()) @map("vector_key")
  documentKey   Int                   @map("document_key")
  featureVector pgvector.Vector(1536)? @map("feature_vector")
  indexState    String                @map("index_state")
  encoderName   String                @map("encoder_name")
  vectorWidth   Int                   @map("vector_width")

  @@map("text_vectors")
}

@prisma/orm-extension-pgvector は、ベクトル型のコーデックやコサイン距離計算などのSQL演算子をPrismaに提供するパッケージです。あくまでPostgreSQL側でベクトル計算を実行するための仕組みであり、テキストから埋め込みベクトルを生成する処理(Embedding生成)自体はOpenAIやGemini等のAPIで行う必要があります。pgvector拡張の公式README

1536 はベクトルの次元数です(利用する埋め込みモデルの出力次元数に合わせて指定します)。なお、拡張パッケージを導入してDBに反映する際は、contract emit に加えて migration plan を実行し、拡張が提供するマイグレーションDDL(CREATE EXTENSION IF NOT EXISTS vector; など)をマイグレーションファイルに含める必要があります。また当然ですが、PostgreSQLサーバー側にpgvector拡張自体がインストールされている必要があります(npmパッケージを追加しただけでDBサーバー側にバイナリが入るわけではありません)。拡張をDBへ反映する手順

距離計算をSQLの式として使う

検索用ベクトルと保存済みベクトルのコサイン距離を求め、類似度の高い順(距離の小さい順)にソートして上位件数を取得する例です。以下は、その処理をPrisma 8のSQLビルダーで表した 解説用の例 です。

const plan = db.sql.public.text_vectors
  .select("vector_key", "document_key")
  .select("distance", (f, fns) =>
    fns.cosineDistance(f.feature_vector, queryVector),
  )
  .where((f, fns) => fns.and(
    fns.eq(f.index_state, "ready"),
    fns.eq(f.encoder_name, encoderName),
    fns.eq(f.vector_width, 1536),
    fns.raw`${f.feature_vector} IS NOT NULL`.returns("pg/bool@1"),
  ))
  .orderBy((f, fns) => fns.cosineDistance(f.feature_vector, queryVector), {
    direction: "asc",
  })
  .limit(10)
  .build();

const rows = await db.runtime().query(plan);

前提として、設定側とランタイム側の双方にpgvector拡張を登録し、queryVector には同じ埋め込みモデルで生成した1536次元の数値配列を渡します。距離が近い順に並べるパターンは公式ガイドにも記載されています。pgvectorの登録と検索

このコードは、Prisma 8の進化を象徴する好例だと感じています。データベース固有の特殊な型をスキーマ上で明示的に宣言し、その型に紐付く演算子(cosineDistance など)を通常のクエリビルダークエリ内でシームレスに扱えるようになっています。通常のCRUD操作とベクトル検索の間で、別のライブラリや専用ツールへ大きく切り替える必要がなくなりました。

なお、本番環境でのベクトル検索では、HNSWやIVFFlatなどのインデックスが正しく効いているかを EXPLAIN の実行計画で確認することが極めて重要です。

デプロイ時の環境依存が大幅に軽減された理由

実運用において非常に助かっているのが、デプロイ環境による差異を意識する負担が大幅に軽減された点です。かつてのPrismaで頻繁に悩まされた、OSやCPUアーキテクチャ(Alpine LinuxやmacOS、AWS Lambdaなど)に合わせたクエリエンジンバイナリの配布やバンドル設定を気にする必要がなくなりました。

ただし、「Prisma 7までは必ずネイティブのQuery Engineバイナリが必要だった」と表現するのは正確ではありません。Prisma 7の段階ですでにドライバーアダプターを標準とする構成へ舵を切っており、従来のネイティブバイナリエンジンを配布しない軽量化が進められていました。Prisma 7へのアップグレード

しかし、Prisma 7の時点ではクエリコンパイラーとして内部的にWebAssembly(WASM)モジュールをロードする仕組みが残っていました。これに対し、Prisma 8ではコアエンジン全体が純粋なTypeScriptで再実装され、WASMコンパイラーへの依存も完全に撤廃されています。Prisma 7のWASMローダー実装、Prisma 8の構成

その結果、Dockerコンテナのビルド時やサーバーレス環境へのバンドル時に発生しがちだった環境固有のトラブルが劇的に減少しました。とはいえ、すべてのランタイム差分が完全に消滅したわけではありません。各データベースドライバーや一部の拡張機能には依然として実行環境の制約が存在します(たとえばSupabase拡張のロール紐付けランタイムはNode.jsとBunが対象となっています)。「どのようなエッジ環境や拡張の組み合わせでも全く同一に動く」と過信せず、対象ランタイムのサポート状況は確認しておく必要があります。

Prisma 7から移ると、どこが良くなり、何を書き換えるのか

基本的なCRUD操作がネストオブジェクトからメソッドチェーンへ刷新

同じモデルに対して一覧取得を行う場合、Prisma 7とPrisma 8の構文を比較するとその違いがよく分かります。

// Prisma 7
const lists = await prisma.readingList.findMany({
  where: { ownerKey },
  select: { listKey: true, title: true },
  take: 20,
});
// Prisma 8
const lists = await db.orm.public.ReadingList
  .where({ ownerKey })
  .select("listKey", "title")
  .limit(20)
  .all();

当然ですが、Prisma 7でもスキーマ定義に基づいた入力・戻り値の型付けは完璧に行われていました。Prisma 8の真価は、この単純なCRUDと同じcontract定義のまま、必要に応じてより低層のSQLビルダー(カラム指定・JOIN・集計)へと滑らかにステップアップできる点にあります。ORMのモデル操作の枠組みに収まらないクエリに出会った際の解決手段が、大きく進化しました。7と8のAPI対応表

コマンドも、役割ごとに分かれる

Prisma 7での操作

Prisma 8で使う主な操作

クライアント生成:prisma generate

型とJSONの生成:prisma contract emit

開発での変更計画・適用:prisma migrate dev

prisma migration plan と prisma db migrate。直接同期する開発用途は prisma db update

配置済み変更の適用:prisma migrate deploy

prisma db migrate

空のDBへの初期反映:prisma db push

prisma db init。その後の直接同期は prisma db update

既存DBからの定義取得:prisma db pull

prisma contract infer

なお、Prisma 8の各CLIコマンドは contract emit によって生成された成果物を読み込んで動作します。そのため、contractファイルを編集した後に emit を実行し忘れると、エディタ上の型定義もマイグレーション計画も古い定義のままになってしまう点に注意してください。CLIの対応表

型名が似ていても、保存型や返却値が変わるものがある

既存アプリを移行する際は、クエリ構文の書き換えだけでなく、データ型のマッピング差異にも注意を払う必要があります。

確認項目

Prisma 8での注意点

JSON

Prisma 7の Json がPostgreSQLで jsonb だった挙動を維持したい場合は、8では明示的に Jsonb を指定します(8の Json は json 型に対応します)

日時

Prisma 8のPSLで記述する DateTime は標準で Temporal.Instant に対応します。従来の Date メソッドを前提とした既存ロジックは修正が必要です

Decimal

文字列として受け取ります。JavaScriptの浮動小数点数(number)へ暗黙的に変換されることはありません

PostgreSQLのenum

既存のネイティブenum型を維持する場合は、native_enum と pg.enum(...) を使用します

多対多リレーション

暗黙の中間テーブル(Implicit many-to-many)に頼っていた関係は、中間モデルを明示的に定義する必要があります

特に注意が必要なのが日時(DateTime)の扱いです。Prisma 8のPSLにおける DateTime は標準で Temporal.Instant 型に対応しており、旧来の Date オブジェクトを前提とした既存ロジックは修正が必要になります(Node.js環境によっては temporal-polyfill の導入も必要です)。また、Prisma 7互換スキーマから読み込む場合は文字列として扱われるなど挙動が異なるため、移行時はAPIから返却されるJavaScript/TypeScriptオブジェクトの実際の型を念入りにテストすることをおすすめします。データ型とJavaScriptの値、7の型からの移行

既存アプリは、7と8を併用して少しずつ移せる

公式のPostgreSQL向け移行ガイドでは、同一のデータベースに対してPrisma 7とPrisma 8を共存させ、エンドポイント単位などで段階的に移行するアプローチが案内されています。設定ファイルやCLIを分離して共存環境を作り、日常のクエリを徐々にPrisma 8へ移行した上で、最終的にマイグレーション管理を引き継ぐ手順をとります。Prisma ORM 7 to 8

既存のPrisma 7プロジェクトにおいて、この共存構成を準備するための初期化コマンドは以下のとおりです。

npx prisma@latest orm init --from-prisma7-schema prisma/schema.prisma

このコマンドは、新規プロジェクト用の雛形を作成するのではなく、既存の prisma/schema.prisma を解析してPrisma 8用のcontractへ変換するためのものです。Prisma 8で構文やサポート状況が変更されたスキーマ要素がある場合は手動調整が必要になるため、公式の移行ガイドを確認しながら慎重に進めてください。Prisma 7プロジェクトでの初期化

また、Prisma 7以外のツールで管理されてきた既存のPostgreSQLデータベースにPrisma 8を導入する場合も、contract infer で既存DBスキーマからcontractのドラフトをリバースエンジニアリングし、レビュー後に contract emit を行い、prisma db sign で現在のDB状態とcontractのハッシュ(signature)を結び付けるという安全な導入ステップが用意されています。いずれのケースでも、既存のテーブルをドロップしたり再作成したりすることなく、段階的に導入することが可能です。既存DBへの追加

Drizzleとの違いは、SQLを書けるかどうかだけでは決まらない

Prisma 8に強力な型安全SQLビルダーが組み込まれたことで、かつてよく言われていた「直感的なORMが好きならPrisma、生のSQLに近い記述がしたいならDrizzle」という単純な二者択一の図式は通用しなくなってきました。

一方で、Drizzleも単なるSQLビルダーにとどまらず、リレーショナルクエリAPI(Relational Queries)やpgvectorの拡張サポート(ベクトル型や距離関数)を標準で備えています。「Prismaだけが型安全」「Drizzleはリレーションを扱えない」といったステレオタイプな比較はもはや適切ではありません。DrizzleのSELECT、リレーショナルクエリ、pgvector

比較にあたっては、執筆時点での各公式ドキュメントを参照しています。DrizzleのリレーショナルAPIの細かな構文はバージョンによって異なるため、古い0.x系のAPIと同一視する形での比較は行っていません。

筆者が選定の際に見ている主な観点は以下のとおりです。

観点

Prisma 7

Prisma 8

Drizzle

定義の中核

Prisma Schemaと生成クライアント

PSLまたはTypeScriptから生成するcontractと共通ランタイム

TypeScriptコードによるテーブル定義

日常のデータ取得

findMany などのモデルAPI

db.orm のメソッドチェーンAPI

SQLライクなAPIおよびリレーショナルクエリAPI

SQL構造の直接構築

生SQLまたはTypedSQLを使用

同一のcontractから db.sql を使用

select().from().where() などで直接構築

ベクトル型(pgvector)

通常のモデルAPIでは制約があり、生SQLを併用

pgvector拡張により専用型と演算子を追加

vectorカラムと距離関数を標準サポート

クエリ変更時のコード生成

通常クエリは不要(TypedSQLのSQL変更時は再生成が必要)

contractが同一であればビルダーの変更だけであればemit不要

通常のビルダークエリにコード生成は一切不要

Prisma 7のベクトル型の扱いは Unsupported database features、Drizzleの定義と生成の流れは Schema および Drizzle Kit generate を参照しました。マイグレーションファイルの生成と、クエリ実行用クライアントの生成は別個の工程です。

Prisma 8は、独立した専用スキーマ言語(PSL)でモデルを俯瞰的に記述し、プラグイン拡張による外部モデルやカスタム型も含めて一元管理したいプロジェクトに最適です。モデルベースのORMと細粒度なSQLビルダーの間で、同一のcontract定義を完全に共有できます。

一方のDrizzleは、テーブル定義からクエリの記述までをすべてTypeScriptの世界で統一し、余計なメタデータ生成ステップを挟まずにSQLライクな記述を徹底したい場合に非常に魅力的な選択肢です。Drizzleでもクエリ結果の型推論は極めて高精度に行われるため、「型安全性を取るか、SQLの表現力を取るか」というトレードオフではなくなっています。なお、生SQLを手書きした部分の安全性や実行時変換への配慮が必要である点は、双方に共通する課題です。Drizzleのsqlテンプレート

同じSELECTを、どこに定義を置いて書くか

Drizzleでは、TypeScriptで記述したテーブル定義オブジェクトを直接インポートしてクエリを組み立てます。たとえば、読書リストを表すテーブル定義は以下のようになります(比較用のサンプルコードです)。

import { pgTable, text, boolean } from "drizzle-orm/pg-core";

export const readingLists = pgTable("reading_lists", {
  listKey: text("list_key").primaryKey(),
  title: text("title").notNull(),
  ownerKey: text("owner_key").notNull(),
  isVisible: boolean("is_visible").notNull().default(false),
});

この定義を、PostgreSQLドライバーで初期化した drizzleDb から利用します。

import { eq } from "drizzle-orm";
import { readingLists } from "./schema";

const rows = await drizzleDb
  .select({ listKey: readingLists.listKey, title: readingLists.title })
  .from(readingLists)
  .where(eq(readingLists.ownerKey, ownerKey))
  .limit(20);
// { listKey: string; title: string }[]

Prisma 8ではcontractから生成された型情報を持つ db.sql.public.reading_lists をクエリの起点とし、DrizzleではTypeScriptで宣言したテーブルオブジェクト readingLists をインポートして .from(...) に渡します。どちらも選択したカラム(射影)に応じて戻り値の型が高精度に推論されますが、「スキーマ定義とその型情報の根拠をどのレイヤーで管理するか」という設計思想に決定的な違いがあります。Drizzleのスキーマ定義、SELECTと部分選択

生成の有無は、クエリとマイグレーションを分けて比較する

コード生成の手間(コードジェネレーション)についても、クエリ記述時とマイグレーション時を切り分けて評価する必要があります。

Prisma 8では、contract(スキーマ定義)を変更した際には contract emit によるメタデータの再生成が必要です。ただし、アプリケーションコード側で select や where の条件をどれだけ変更しても、追加の生成コマンドは一切不要です(TypedSQLのようにSQLの変更ごとにコマンドを再実行する必要はありません)。

一方のDrizzleは、通常のクエリ開発においてクライアントコード生成の手間が完全にゼロです。ただし、データベースへ適用するマイグレーションファイルを生成する場面では、DrizzleでもDrizzle Kitによる生成ステップ(drizzle-kit generate)が必要となります。「クライアントコードの生成が不要であること」と「マイグレーションDDLの生成が不要であること」は全く別の話です。Drizzle Kit generate、Prisma contract emit

ベクトル検索やraw SQLでも、単純な優劣にはならない

DrizzleにもPostgreSQLのvector型と cosineDistance などの距離関数が備わっており、HNSW等のインデックスも定義可能です。したがって、ベクトル検索ができること自体はPrisma 8だけの優位点ではありません。Prisma 8で注目すべきなのは、カスタム型・SQL演算・関連するマイグレーションDDLを「拡張パッケージ」という単位にカプセル化し、contractとクエリの双方へシームレスに組み込めるアーキテクチャです。たとえばpgvector拡張をDB側で有効化するDDLは、Prismaでは拡張パッケージのマイグレーションから自動的に取り込まれます。一方、Drizzleの公式ガイドではカスタムマイグレーション内に手動で CREATE EXTENSION vector; を記述する手順が案内されています。Drizzleのvector検索、Prismaの拡張

また、生SQLに対する型安全性についても、双方が宣言した型の正しさをDBエンジンが保証してくれるわけではありません。Drizzleの sql<T> はTypeScriptコンパイラに型を伝える注釈であり、実行時のデータ型変換は .mapWith(...) などを別途指定します。Prisma 8でも、rawフラグメントの .returns(...) と文全体の .returnsRow(...) とで役割が異なります。「コード上で型注釈が書けること」「記述したSQLが構文的に正しいこと」「取得データが意図したJavaScript型に安全にデシリアライズされること」の3点は、いずれのツールを採用する場合でも切り分けて検証する必要があります。DrizzleのsqlとmapWith、Prismaのraw SQL

自分なら、データ定義をどう運用したいかで選ぶ

PSLを用いてモデルの全体像を一元管理し、リレーションを辿る高水準なORM APIを主軸にしつつ、複雑な要件でのみSQLビルダーへ滑らかに移行したいチームには、Prisma 8が非常にフィットします。外部サービスのスキーマやデータベース固有の特殊型を拡張パッケージとして取り込める拡張性も大きな魅力です(なお、contractはTypeScriptでも記述可能なため、PSLの採用が必須というわけでもありません)。

一方、テーブル定義からクエリ構築に至るまでをすべてTypeScriptのコードとして完結させ、通常のデータアクセス時にメタデータの生成ステップを一切挟みたくない場合は、Drizzleが極めて自然な選択肢となります。Drizzleにも洗練されたリレーショナルAPIが備わっているため、常に複雑なJOINを手書きしなければならないわけではありません。

この比較は機能的な特徴を踏まえた設計上の判断基準であり、どちらか一方の性能やバンドルサイズが絶対的に優れていると断定するものではありません。実際の選定にあたっては、利用するデータベースエンジン、ドライバー、拡張機能の成熟度、既存コード資産の移行コストなどを総合的に考慮して判断することをおすすめします。

RC時点の制約を確認してから採用する

Prisma 8は、日常的なCRUD操作の簡潔さと、高度な要件に応えるSQL表現力とを高い水準で両立させたORMに仕上がっています。モデルを宣言して普段の操作は直感的に書き、検索や集計が複雑化した際には同一のデータ定義から安全にSQLを組み立てる。さらにデータベース固有の拡張機能もプラグインとして取り込める。この一貫した開発体験は非常に魅力的です。

PythonのSQLAlchemyに似た感覚と述べたのも、内部の実行モデルが同一という意味ではなく、「高水準なモデル操作の利便性」と「SQLの式を自在に組み立てる表現力」をシームレスに行き来できるという実用上のフィーリングを指しています。TypeScriptの恩恵により、入力パラメータから戻り値に至るまで型推論が途切れない点も大きな強みです。

ただし、Prisma 7で利用できたすべての機能がそのままの形で引き継がれているわけではありません。現在のRC版には、JSON内部プロパティに対する詳細なフィルター、旧来の atomic increment、多くの nested write 操作、トランザクションの明示的な分離レベル指定など、現時点で直接の代替機能が提供されていない項目もあります。また、従来の $extends の役割も、ミドルウェアやカスタムCollectionなどの新しい仕組みへと再編されています。Prisma 7からの対応表と未対応機能

対応データベースについても現段階では差があります。現在のPrisma 8はPostgreSQLを中心として開発されており、MongoDBはEarly Access、SQLiteはExperimentalの位置付けです。MySQL、SQL Server、CockroachDBなどを本番運用している既存アプリケーションでは、Prisma 7と同等のサポート範囲をそのまま前提とすることはできません。Supported databases

また、マイグレーション機能もcontract間の遷移履歴をグラフ構造として追跡するTypeScriptベースの方式に刷新されており、単に依存ライブラリのバージョン番号を引き上げるだけでは移行できません。How migrations work

公式方針として、Prisma 7はPrisma 8の正式一般公開(GA)後も18か月間にわたりバグ修正とセキュリティパッチの提供が継続されます。既存プロジェクトで利用している機能に未対応のものが含まれている場合は、無理に移行せずPrisma 7を維持して成熟を待つのが賢明な判断です。移行を検討する場合も、新規プロジェクト用の初期化コマンドを本番DBへ安易に実行せず、公式の共存手順に沿って段階的に検証を進めることが不可欠です。7のサポート方針

このように、Prisma 8は極めて高い完成度を見せていますが、既存のあらゆるプロジェクトに対して無条件に即時移行を推奨できる段階かというと、要件に応じた冷静な見極めが必要です。

何より大きな進化は、単なる機能の追加にとどまらず、実装中にORMとSQLの間で思考や構文を断絶させずに済む体験の良さにあります。日常のCRUDから動的検索、外部スキーマ連携、ベクトル検索に至るまで、同一のcontract定義を起点として型安全に書き分けられる柔軟性は、今後のTypeScriptバックエンド開発における強力な選択肢となるはずです。