prisma vs typeorm 徹底比較

prisma の詳細typeorm の詳細
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Prisma vs TypeORM — Node.js/TypeScript ORM 徹底比較

1. 結論

新規プロジェクトで型安全性と開発体験(DX)を最優先するなら Prisma、ActiveRecord パターンや既存の RDB 設計との親和性を重視するなら TypeORM を選ぶのがおすすめです。どちらも本番運用に耐えうる成熟した ORM ですが、設計思想が大きく異なるため、プロジェクトの性質やチームの経験に合わせて選択することが重要です。


2. 比較表

観点PrismaTypeORM
設計パターン独自のクエリエンジン + スキーマ駆動Data Mapper / Active Record
スキーマ定義独自DSL(.prisma ファイル)TypeScript デコレータ / エンティティクラス
型安全性◎ スキーマから自動生成、クエリ結果まで完全型付け○ エンティティ型はあるが、クエリ結果の型推論は限定的
TypeScript 対応◎ ファーストクラス○ ファーストクラス(ただし any が混入しやすい)
マイグレーションprisma migrate(SQL 自動生成)CLI による生成 + 手動編集
対応 DBPostgreSQL, MySQL, MariaDB, SQLite, SQL Server, CockroachDB, MongoDB (Preview)MySQL, MariaDB, PostgreSQL, SQLite, MS SQL Server, Oracle, SAP HANA, MongoDB
Raw SQL$queryRaw / $executeRawquery() / 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 で生成されるクライアントは、selectinclude の指定に応じて 戻り値の型が動的に変化 します。これは 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 を導入TypeORMsynchronize: false + 手動マッピングで既存スキーマに柔軟に対応
複雑な SQL(サブクエリ・CTE・UNION)TypeORMQueryBuilder が SQL に近く、複雑なクエリを組み立てやすい
型安全性を最大限に活かしたいPrismaselect / include に応じた戻り値型の自動推論は唯一無二
NestJS プロジェクトどちらでも可両方とも公式統合あり。チームの好みで選択
Oracle / SAP HANA を使うTypeORMPrisma は未対応
MongoDB をメインで使うどちらも慎重にPrisma は Preview、TypeORM も MongoDB 対応は限定的。Mongoose を検討
マイクロサービス / サーバーレスPrismaPrisma Accelerate によるコネクションプーリング、エッジ対応が進んでいる
チームに Rails / Laravel 経験者が多いTypeORMActive Record パターンが馴染みやすい

6. まとめ

Prisma  → 「スキーマを書けば、あとは全部生成してくれる」