データベース

TypeORMとPrismaの違いと選び方|型安全・発行SQLを実測【2026年10月】

TypeORMとPrismaの違いと選び方|型安全・発行SQLを実測【2026年10月】

TypeORMとPrismaは、どちらもNode.jsとTypeScriptで使うORMです。TypeORMはクラスとデコレーターでエンティティを書き、Prismaはschema.prismaから型付きのクライアントを生成します。比較記事を読む際は対象バージョンの確認が必要で、Prisma公式の比較ページでさえTypeORM側のコードは1.0で削除されたgetManager()を使っています。

この記事では、2026年10月時点の安定版であるTypeORM 1.1.2とPrisma 7.10.1をPostgreSQL 18.6に接続し、同じ構造のモデルで型エラー・発行SQL・トランザクション・マイグレーションを実行して比べました。掲載している出力は、その実行結果を抜粋したものです。検証環境はmacOS、Node.js v26.5.0、TypeScript 7.0.2で、ユーザー3件と各ユーザーの投稿2件を用意しました。SQL例では取得列などを省略しています。

まとめ:TypeORMとPrismaの違いと選び方の結論

新しく作るなら、型の守りが厚いPrismaを先に検討します。今回の検証では、存在しない列名や型の合わない演算子はどちらのORMもコンパイル時に止めました。差が出たのは「selectで取らなかった列」と「読み込んでいない関連」で、Prismaは型エラーにし、TypeORMは型上は存在する扱いのまま実行時にundefinedを返しました。

一方、where条件の値がundefinedのまま更新すると、今回のPrismaのupdateManyは既定で全行を更新し、TypeORM 1.1.2のupdateは例外で止めました。安全側の既定値は項目ごとに逆なので、どちらを選んでも後述の設定を入れてから使います。

観点 Prisma 7.10.1 TypeORM 1.1.2
モデル定義 schema.prisma(専用言語) クラス+デコレーター
取らなかった列・関連の型 型エラーになる 型上は存在する
関連の取得 今回の1関連取得:既定2本/relationJoinsで1本 今回のjoin方式:1本(take併用で2本)
undefinedのwhereで更新 全行を更新(既定) 例外で停止(1.0から)
ページング skip/take+cursor skip/take
マイグレーション migrate dev/deploy(downなし) migration:generate/run(down付き)
向くチーム 新規開発・型で守りたい 既存資産・SQLを細かく書く

以下、版の前提、モデル定義、型安全、クエリ、更新の安全性、マイグレーション、移行、選び方の順に根拠を示します。

2026年10月時点のTypeORMとPrismaの版

TypeORM 1.1系と0.3系の位置づけ

TypeORMは0.3系が長く続いたあと、1.0.0を2026年5月19日に公開しました。1.0ではConnectionが削除されてDataSourceに一本化され、findオプションの文字列配列によるselect・relations指定もなくなりました。0.3からの移行ガイドでは、Node.js 20以上が必要になったこと、npx @typeorm/codemod v1 src/で書き換えの大半を自動化できることが案内されています。

その後はGitHubのリリース一覧のとおり、1.1.0(2026-07-13)、1.1.1(09-01)、1.1.2(10-08)と続いています。npmのlatestタグは1.1.2、legacyタグは0.3.31(07-13)です。0.3系にも1.1.0と同じ日に修正版が出ていますが、新規で入れるなら1.1系を選びます。

Prisma 7と、npmのlatestが指すPrisma 8 RC

Prisma 7.0.0は2025年11月19日にnpmへ公開されました。v7への移行ガイドによると、Rustのクエリエンジンを使わないprisma-clientジェネレーターが標準になり、DBへ直接接続する場合は@prisma/adapter-pgなどのdriver adapterが必須になりました。Prisma Accelerate経由では別の接続設定を使います。ジェネレーターのoutputも必須です。

注意したいのは、2026年10月29日時点でnpmのprismaパッケージのlatestタグが8.0.0-rc.28を指していることです。公式のリリース状況ページは、Prisma 8のCLIはschema.prismaを読まず、generate・migrate dev・db pushも持たないと警告しています。7で作るならnpm install --save-dev prisma@7のように版を指定してください。7は8の正式版から18か月保守され、6のセキュリティ修正は2026年11月19日で終わります。版の確認方法と固定の手順はPrismaのバージョン一覧と7への固定にまとめています。

モデル定義の違い:schema.prismaとデコレーター付きクラス

検証には、ユーザーと投稿が1対多のモデルを使いました。関連の構造は共通ですが、外部キーのNULL許容と参照アクションは各ORMの既定値を使っており、DB制約は同一ではありません。

Prisma:schema.prismaによる型付きクライアントの生成

generator client {
  provider        = "prisma-client"
  output          = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
  posts Post[]
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  authorId Int
  author   User   @relation(fields: [authorId], references: [id])
}

Prisma 7では接続URLをschema.prismaではなくprisma.config.tsのdatasource.urlに書きます。npx prisma generateを実行すると、outputに指定した場所へクライアントと型が出力されます。

TypeORM:クラスとデコレーターによるエンティティ定義

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column("text", { unique: true })
  email!: string;

  @Column("text")
  name!: string;

  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];
}

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column("text")
  title!: string;

  @ManyToOne(() => User, (user) => user.posts)
  author!: User;
}

列の型を"text"と明示しているのには理由があります。TypeORMのCLIをtsx経由で動かしたところ、@Column()のままではColumnTypeUndefinedError: Column type for User#email is not defined and cannot be guessedで止まりました。TypeORMはemitDecoratorMetadataが出力する型情報から列の型を推測しますが、tsxが使うesbuildはこのオプションに対応していません。tscでビルドする構成なら省略できますが、tsxやVitestで動かすなら型を書いておくほうが確実です。エンティティの書き方はTypeORM 1.1系の使い方で詳しく扱っています。

型安全の差:tscで確かめた5つの書き間違い

Prisma公式の比較ページは、TypeORMでは数値の列にMoreThan("test")を渡しても、存在しない列をwhereに書いてもコンパイルが通ると説明しています。そこで同じ種類の書き間違いを両方のORMで書き、TypeScript 7.0.2のtsc --strictに通しました。

書き間違い Prisma 7.10.1 TypeORM 1.1.2
whereに存在しない列名 TS2353で検出 TS2353で検出
文字列の列に数値の比較 TS2322で検出 TS2322で検出
数値の列にMoreThan(“test”) (該当APIなし) TS2322で検出
selectで取らなかった列を読む TS2339で検出 通る(実行時undefined)
読み込んでいない関連を読む TS2339で検出 通る(実行時undefined)

公式比較ページが挙げる2つの例は、TypeORM 1.1.2だけでなく0.3.31でもコンパイルエラーになりました。ページの例は0.2時代のAPIで書かれており、現行のTypeORMには当てはまりません。

差が残っているのは表の下2行です。次のコードはTypeORMではエラーなしでコンパイルできます。

const u = await users.findOneOrFail({ where: { id: 1 }, select: { name: true } });
const e: string = u.email;          // 型はstring、実行するとundefined

const v = await users.findOneByOrFail({ id: 1 });
const len: number = v.posts.length; // relations未指定なのでpostsはundefined

今回使ったTypeORMのfind系メソッドはエンティティクラスの型を返すため、取得列の選択が戻り値の型に反映されません。Prismaはselectやincludeの指定から戻り値の型を組み立てるため、同じ書き方でProperty 'email' does not exist on type '{ name: string; }'になります。TypeORMで同じ守りが欲しいなら、部分取得の結果を専用の型(DTO)に詰め替えるのが現実的です。

部分一致(LIKE)検索の書き方と発行SQL

名前に「al」を含むユーザーを、大文字小文字を区別せずに探すコードと、実際に発行されたSQLです。

// Prisma
await prisma.user.findMany({ where: { name: { contains: "al", mode: "insensitive" } } });
// SELECT ... FROM "public"."User" WHERE "public"."User"."name" ILIKE ('%' || $1 || '%')

// TypeORM
await users.find({ where: { name: ILike("%al%") } });
// SELECT ... FROM "user" "User" WHERE (("User"."name" ILIKE $1)) -- PARAMETERS: ["%al%"]

どちらもPostgreSQLのILIKEになり、結果はAliceとalanの2件で同じでした。Prismaはcontains・startsWith・endsWithで意図を書き、ワイルドカードはSQL側で付けます。TypeORMは%を自分で書きます。Prismaのmode: "insensitive"はAPIリファレンス上はPostgreSQLとMongoDBのコネクターが対象ですが、Prisma 7はMongoDBに対応していないため、実際に使えるのはPostgreSQLです。MySQLではDBの照合順序で大文字小文字の扱いが決まります。

ビューや集計を含む複雑な条件は、TypeORMならQueryBuilder、Prismaなら$queryRawか、SQLファイルから型を生成するTypedSQL(プレビュー機能)で書きます。

関連データの取得:relationJoinsの有無で変わるクエリ数

Prisma:既定は2クエリ、relationJoinsで1クエリ

ユーザー2人と各自の投稿をincludeで取ると、Prisma 7.10.1は既定で次の2本を発行しました。

SELECT ... FROM "public"."User" WHERE 1=1 ORDER BY "public"."User"."id" ASC LIMIT $1 OFFSET $2
SELECT ... FROM "public"."Post" WHERE "public"."Post"."authorId" IN ($1,$2) OFFSET $3

1本にまとめるrelationLoadStrategy: "join"は、そのまま書くとUnknown argument `relationLoadStrategy`で実行時に失敗しました。公式ドキュメントのとおり、この機能はまだプレビューで、ジェネレーターにpreviewFeatures = ["relationJoins"]を足してprisma generateし直す必要があります。有効にすると指定しなくてもjoinが既定になり、LEFT JOIN LATERALとJSONB_AGGで関連をJSONに組み立てる1本のSQLに変わりました。対応DBはPostgreSQL・CockroachDB・MySQLです。

TypeORM:LEFT JOINの1クエリ、take併用で2クエリ

TypeORMのrelations: { posts: true }はLEFT JOINの1本です。ただしtake: 2を付けると、JOINで行が増えてもユーザー2人を正しく数えるために、先にSELECT DISTINCTのサブクエリでIDを2件取り、続けてWHERE "User"."id" IN (1, 2)で本体を取る2本になりました。どちらのORMも、関連をループの中で1件ずつ取るとN+1になる点は同じです。検出と対策はN+1問題の発行クエリ数の実測で扱っています。

ページングの書き方:skip/takeとカーソル

両者ともskipとtakeでオフセット型のページングができます。違いはカーソル型で、Prismaはcursorオプションを持ちます。

await prisma.user.findMany({ cursor: { id: lastId }, skip: 1, take: 2, orderBy: { id: "asc" } });
// WHERE "public"."User"."id" >= (SELECT "public"."User"."id" FROM "public"."User" WHERE ("public"."User"."id") = ($1))
// ORDER BY "public"."User"."id" ASC LIMIT $2 OFFSET $3

カーソルの行自体も条件に含まれるので、公式の例どおりskip: 1で1行読み飛ばします。TypeORMにはカーソル専用のオプションがないため、where: { id: MoreThan(lastId) }とorderを自分で組みます。並べ替えの列は一意である必要があります。

where未指定の更新・削除で起きること

フォームからIDが届かず、where条件の値がundefinedのまま更新を呼んだ場合を両方で試しました。

const input: { id?: number } = {};

// Prisma 7.10.1
await prisma.user.updateMany({ where: { id: input.id }, data: { name: "X" } });
// => count= 3(全ユーザーの名前がXになった)

// TypeORM 1.1.2
await users.update({ id: input.id }, { name: "X" });
// => TypeORMError: Undefined value encountered in property 'id' of a where condition.

Prismaはundefinedを「何もしない」として扱うため、条件が消えて全3行が更新されました。公式もデータ消失につながると警告し、プレビュー機能のstrictUndefinedChecksを推奨しています。ジェネレーターに追加して再生成すると、同じコードがexplicitly `undefined` values are not allowedで止まりました。Prismaを本番で使うなら、最初に入れておきたい設定です。

TypeORMは1.0でinvalidWhereValuesBehaviorの既定が例外に変わりました。update({}, ...)のような空条件もEmpty criteria(s) are not allowed for the update method.で拒否されます。0.3系から上げたコードで例外が増えた場合、この変更が原因のことがあります。

マイグレーションの生成と適用の違い

# Prisma 7
npx prisma migrate dev --name init   # 開発DBに差分SQLを作って適用
npx prisma generate                  # 7からはmigrate devが自動で実行しない
npx prisma migrate deploy            # 本番に未適用分を適用

# TypeORM 1.1(tsxでTypeScriptの設定を読む例)
npx tsx node_modules/typeorm/cli.js migration:generate -d src/data-source.ts src/migrations/Init
npx tsx node_modules/typeorm/cli.js migration:run -d src/data-source.ts

同じモデルから生成されたものを比べると、違いは3点ありました。

  • Prismaはmigration.sqlだけを作り、戻すためのSQLはありません。必要ならmigrate diffで自分で作ります。TypeORMはup()とdown()の両方を持つクラスを生成しました。
  • 外部キーは、PrismaがON DELETE RESTRICT ON UPDATE CASCADE、TypeORMがON DELETE NO ACTION ON UPDATE NO ACTIONでした。
  • TypeORMの@ManyToOneは既定でNULLを許すため、authorId列がinteger(NOT NULLなし)になりました。必須にするならnullable: falseを付けます。

Prisma 7で見落としやすいのは、migrate devのあとにクライアントが更新されないことです。実際にmigrate devだけではgeneratedフォルダーが作られず、prisma generateを別に実行する必要がありました。移行ガイドにも同じ記載があります。開発中の素早い反映にはdb pushも使えますが、履歴が残らないので本番の手順には入れません。

トランザクションの書き方

どちらもコールバック型で、中で例外を投げるとロールバックされます。ユーザーを1件作ってから例外を投げると、両方とも件数は3件のまま変わりませんでした。

// Prisma(インタラクティブトランザクション)
await prisma.$transaction(async (tx) => {
  await tx.user.create({ data: { email: "[email protected]", name: "Tx" } });
  throw new Error("boom");
});

// TypeORM
await dataSource.transaction(async (manager) => {
  await manager.save(User, { email: "[email protected]", name: "Tx" });
  throw new Error("boom");
});

Prismaは処理の配列を$transaction([...])に渡す書き方もあり、互いに依存しない書き込みをまとめるだけならこちらが簡潔です。TypeORMはQueryRunnerでstartTransaction・commitTransaction・rollbackTransactionを手で呼ぶこともできます(TypeORMのトランザクション)。なお、TypeORMのsave()は単独で呼んでも毎回START TRANSACTIONとCOMMITで包まれていました。

TypeORMからPrismaへ移行する手順と注意点

既存のTypeORMプロジェクトは、DBを残したままPrismaに切り替えられます。公式の移行ガイドに沿った流れは次のとおりです。

  1. npx prisma db pullで既存DBからschema.prismaを作る
  2. モデル名と項目名を整え、@@map・@mapで実際のテーブル名・列名に対応づける
  3. ベースラインとして初回マイグレーションを作り、prisma migrate resolve --appliedで適用済みにする
  4. prisma generateでクライアントを生成し、driver adapterを渡して初期化する。その後、クエリを画面や機能の単位で少しずつ書き換え、しばらく両方を共存させる

今回の検証では、TypeORMはテーブル名を"user"・"post"と小文字で作り、Prismaは"User"・"Post"で作りました。db pullで得た小文字のモデル名もそのまま使えます。モデル名をPascalCaseに整えたい場合に、2の作業を行います。注意したいのは前の章のundefinedの扱いで、TypeORMの例外に守られていた更新処理がPrismaでは全行更新になり得ます。移行するならstrictUndefinedChecksを先に入れておきます。QueryBuilderや生SQLに頼る処理が多いほど、書き換えの量は増えます。

TypeORMとPrismaの選び方:採用条件と避ける場面

Prismaを選ぶ条件:新規開発で、取得した列や関連の有無まで型で守りたい場合です。パッケージを使わずに業務システムを一から作るフルスクラッチ開発では、テーブルと関連が増えるほど、関連の取り忘れがコンパイル時に分かる利点が大きくなります。ただし2026年10月はPrisma 8への移行期なので、7に版を固定し、strictUndefinedChecksと必要ならrelationJoinsを有効にして使います。

TypeORMを選ぶ条件:すでにTypeORMで動いているシステム、NestJSの既存構成、QueryBuilderでSQLを細かく組みたい場合です。1.0以降は活発に更新され、where未指定の更新を既定で止めるなど安全側の改善も入っています。部分取得の結果は専用の型に詰め替える、と決めておけば型の弱点を補えます。

どちらも避ける場面:SQLの形を自分で決めたいが、ORMの抽象化は要らないという場合は、型付きのクエリビルダーのほうが合います。候補はDrizzle ORMとPrismaの比較を参照してください。ORMそのものの利点と限界はORMの仕組みとメリット・デメリットで整理しています。

よくある質問

TypeORMとPrismaはどちらが速い?

今回測定したのは発行SQLの本数と形で、処理時間の優劣は判定していません。速度を比較するには、同じデータ量と負荷で応答時間も測定する必要があります。今回の検証では、関連付きの一覧取得はPrismaが既定で2本、relationJoins有効で1本、TypeORMはtakeなしで1本、takeありで2本でした。どちらもログでSQLを確認し、ループ内のクエリ(N+1)をなくすことが先です。

PrismaでLIKE検索(部分一致)はどう書く?

whereにcontainsを書きます。前方一致はstartsWith、後方一致はendsWithです。大文字小文字を区別しないならmode: "insensitive"を足し、PostgreSQLではILIKEが発行されます。%を自分で書く必要はありません。

TypeORMは開発が止まっているのでは?

止まっていません。2026年5月19日に1.0.0、7月13日に1.1.0、10月8日に1.1.2が公開されています。0.3系が長く続いた時期の印象が残っていますが、2026年は1.x系に移っています。

prisma migrate devとmigrate deployの違いは?

migrate devは開発用で、スキーマの差分からマイグレーションのSQLを作って開発DBに適用します。migrate deployは本番用で、新しいSQLは作らず、未適用のマイグレーションを順に適用するだけです。Prisma 7ではmigrate devのあとにprisma generateを別に実行します。

PrismaのスキーマDSL(schema.prisma)とは?

modelとフィールド、@id・@unique・@relationなどの属性でテーブルと関連を宣言するPrisma専用の言語です。このファイルからクライアントの型とマイグレーションの両方が作られます。なお、Prisma 8のCLIはschema.prismaを読まないため、8へ上げる際は書き方が変わります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  5. 2026.10.28 テックブログ 楽天トラベル不正アクセス:最大約260万件の対象確認とカード下4桁の詐欺対策

RELATED POSTS 関連記事

目次