Prisma vs TypeORM — Node.js/TypeScript ORM 徹底比較
1. 結論
新規プロジェクトで型安全性と開発体験(DX)を最優先するなら Prisma、ActiveRecord パターンや既存の RDB 設計との親和性を重視するなら TypeORM を選ぶのがおすすめです。どちらも本番運用に耐えうる成熟した ORM ですが、設計思想が大きく異なるため、プロジェクトの性質やチームの経験に合わせて選択することが重要です。
2. 比較表
| 観点 | Prisma | TypeORM |
|---|---|---|
| 設計パターン | 独自のクエリエンジン + スキーマ駆動 | Data Mapper / Active Record |
| スキーマ定義 | 独自DSL(.prisma ファイル) | TypeScript デコレータ / エンティティクラス |
| 型安全性 | ◎ スキーマから自動生成、クエリ結果まで完全型付け | ○ エンティティ型はあるが、クエリ結果の型推論は限定的 |
| TypeScript 対応 | ◎ ファーストクラス | ○ ファーストクラス(ただし any が混入しやすい) |
| マイグレーション | prisma migrate(SQL 自動生成) | CLI による生成 + 手動編集 |
| 対応 DB | PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, CockroachDB, MongoDB (Preview) | MySQL, MariaDB, PostgreSQL, SQLite, MS SQL Server, Oracle, SAP HANA, MongoDB |
| Raw SQL | $queryRaw / $executeRaw | query() / QueryBuilder |
| リレーション | スキーマで宣言的に定義、include / select で取得 | デコレータで定義、relations / QueryBuilder で取得 |
| GUI ツール | Prisma Studio(組み込み) | なし(外部ツール利用) |
| npm 週間DL数(2024年時点) | 約 250 万 | 約 180 万 |
| GitHub Stars | ≈ 40k | ≈ 34k |
| バンドルサイズ(node_modules) | 大きめ(Rust 製クエリエンジンバイナリ含む) | 中程度 |
| 学習コスト | 中(独自 DSL の習得が必要) | 中〜高(デコレータ・パターンの理解が必要) |
| NestJS 統合 | 公式モジュールあり | 公式推奨 ORM の一つ |
3. それぞれの強み
Prisma の強み
- 圧倒的な型安全性:
prisma generateで生成されるクライアントは、selectやincludeの指定に応じて 戻り値の型が動的に変化 します。これは TypeORM では実現できないレベルの型推論です。 - 宣言的スキーマ(Single Source of Truth):
.prismaファイルにモデル定義を一元管理でき、マイグレーション・型生成・ER 図すべてがここから派生します。 - Prisma Studio:
npx prisma studioだけでブラウザベースの DB 管理 GUI が起動し、データの閲覧・編集が可能です。 - マイグレーションの安全性:
prisma migrate devはスキーマ差分から SQL を自動生成し、prisma migrate deployで本番適用するワークフローが明確です。 - 活発な開発・エコシステム: Prisma Accelerate(コネクションプーリング)、Prisma Pulse(リアルタイムイベント)など、周辺サービスの拡充が進んでいます。
TypeORM の強み
- 柔軟なクエリ構築: QueryBuilder は SQL に近い記法で複雑なクエリを組み立てられるため、サブクエリ・UNION・ウィンドウ関数 など高度な SQL 操作に対応しやすいです。
- Active Record パターンのサポート: エンティティに
save()/remove()を直接生やせるため、Rails や Laravel 経験者には馴染みやすい設計です。 - 対応 DB の広さ: Oracle や SAP HANA など、エンタープライズ系 DB への対応は TypeORM の方が充実しています。
- デコレータベースの定義: TypeScript のクラスとデコレータでエンティティを定義するため、コードとスキーマが同じ言語 で完結します。独自 DSL の学習が不要です。
- 既存 DB との親和性:
synchronizeオプションや手動マイグレーションにより、既存のレガシー DB スキーマに合わせた柔軟なマッピングが可能です。
4. コード例で比較
以下では「ユーザーと投稿(1対多)」を題材に、同じ操作を両方の ORM で実装します。
4-1. スキーマ / エンティティ定義
Prisma(schema.prisma)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
TypeORM(エンティティクラス)
// user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
} from "typeorm";
import { Post } from "./post.entity";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true })
email!: string;
@Column({ nullable: true })
name!: string | null;
@OneToMany(() => Post, (post) => post.author)
posts!: Post[];
@CreateDateColumn()
createdAt!: Date;
}
// post.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
} from "typeorm";
import { User } from "./user.entity";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column({ nullable: true })
content!: string | null;
@Column({ default: false })
published!: boolean;
@ManyToOne(() => User, (user) => user.posts)
author!: User;
@CreateDateColumn()
createdAt!: Date;
}
4-2. CRUD 操作
ユーザー作成 + 投稿を同時作成(ネストした create)
Prisma
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// ユーザーと投稿をネストして一括作成
const user = await prisma.user.create({
data: {
email: "alice@example.com",
name: "Alice",
posts: {
create: [
{ title: "初めての投稿", content: "Hello, Prisma!" },
{ title: "2番目の投稿", published: true },
],
},
},
include: { posts: true }, // ← 戻り値に posts が型付きで含まれる
});
console.log(user.posts[0].title); // 型安全: string
TypeORM
import { DataSource } from "typeorm";
import { User } from "./user.entity";
import { Post } from "./post.entity";
const dataSource = new DataSource({
type: "postgres",
url: process.env.DATABASE_URL,
entities: [User, Post],
synchronize: true, // 開発用のみ
});
await dataSource.initialize();
const userRepo = dataSource.getRepository(User);
const postRepo = dataSource.getRepository(Post);
// ユーザー作成
const user = userRepo.create({
email: "alice@example.com",
name: "Alice",
});
await userRepo.save(user);
// 投稿を個別に作成してリレーション設定
const posts = postRepo.create([
{ title: "初めての投稿", content: "Hello, TypeORM!", author: user },
{ title: "2番目の投稿", published: true, author: user },
]);
await postRepo.save(posts);
// リレーション込みで再取得
const userWithPosts = await userRepo.findOne({
where: { id: user.id },
relations: { posts: true },
});
console.log(userWithPosts!.posts[0].title); // string
ポイント: Prisma はネストした
createを 1 回の API コールで実行でき、戻り値の型もincludeに応じて自動推論されます。TypeORM では作成とリレーション取得が分離しがちです。
条件付き検索 + ページネーション
Prisma
// 公開済み投稿をタイトル部分一致で検索(ページネーション付き)
const posts = await prisma.post.findMany({
where: {
published: true,
title: { contains: "Prisma", mode: "insensitive" },
},
include: { author: { select: { name: true, email: true } } },
orderBy: { createdAt: "desc" },
skip: 0,
take: 10,
});
// posts の型:
// { id: number; title: string; ...; author: { name: string | null; email: string } }[]
TypeORM(QueryBuilder)
const posts = await postRepo
.createQueryBuilder("post")
.leftJoinAndSelect("post.author", "author")
.select(["post", "author.name", "author.email"])
.where("post.published = :published", { published: true })
.andWhere("post.title ILIKE :title", { title: "%TypeORM%" })
.orderBy("post.createdAt", "DESC")
.skip(0)
.take(10)
.getMany();
// posts の型: Post[](author の型は部分的にしか絞れない)
ポイント: Prisma は
select/includeの指定がそのまま戻り値の型に反映されます。TypeORM の QueryBuilder は柔軟ですが、部分 select 時の型推論が弱く、実行時にundefinedになるフィールドが型上は存在するように見えることがあります。
トランザクション
Prisma
const [user, post] = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: "bob@example.com", name: "Bob" },
});
const post = await tx.post.create({
data: { title: "トランザクション内投稿", authorId: user.id },
});
return [user, post] as const;
});
TypeORM
await dataSource.transaction(async (manager) => {
const user = manager.create(User, {
email: "bob@example.com",
name: "Bob",
});
await manager.save(user);
const post = manager.create(Post, {
title: "トランザクション内投稿",
author: user,
});
await manager.save(post);
});
両者ともコールバック形式のトランザクションをサポートしており、使い勝手に大きな差はありません。
5. どちらを選ぶべきか — ユースケース別の推奨
| ユースケース | 推奨 | 理由 |
|---|---|---|
| 新規プロジェクト(グリーンフィールド) | Prisma | スキーマ駆動でマイグレーション・型生成が一気通貫。DX が高い |
| 既存 DB にあとから ORM を導入 | TypeORM | synchronize: false + 手動マッピングで既存スキーマに柔軟に対応 |
| 複雑な SQL(サブクエリ・CTE・UNION) | TypeORM | QueryBuilder が SQL に近く、複雑なクエリを組み立てやすい |
| 型安全性を最大限に活かしたい | Prisma | select / include に応じた戻り値型の自動推論は唯一無二 |
| NestJS プロジェクト | どちらでも可 | 両方とも公式統合あり。チームの好みで選択 |
| Oracle / SAP HANA を使う | TypeORM | Prisma は未対応 |
| MongoDB をメインで使う | どちらも慎重に | Prisma は Preview、TypeORM も MongoDB 対応は限定的。Mongoose を検討 |
| マイクロサービス / サーバーレス | Prisma | Prisma Accelerate によるコネクションプーリング、エッジ対応が進んでいる |
| チームに Rails / Laravel 経験者が多い | TypeORM | Active Record パターンが馴染みやすい |
6. まとめ
Prisma → 「スキーマを書けば、あとは全部生成してくれる」