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に切り替えられます。公式の移行ガイドに沿った流れは次のとおりです。
npx prisma db pullで既存DBからschema.prismaを作る- モデル名と項目名を整え、
@@map・@mapで実際のテーブル名・列名に対応づける - ベースラインとして初回マイグレーションを作り、
prisma migrate resolve --appliedで適用済みにする 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へ上げる際は書き方が変わります。